这是一篇关于JavaScript原型知识的还债帖

前言

本人从事前端开发的工做三年有余,我要向你坦白,时至今日我对JS原型仍然是只知其一;不知其二,当年的校招面试关于JS原型都是“临时抱佛脚”,死记硬背混过去鸟~ ~。html

在往后工做中,我已熟练的使用Function去封装类,使用mixin去丰富类,使用new去实例化我钟意的对象(单身狗的悲哀),然而却忘了它们背后蕴含的原理。前端

痛定思痛,本文算做是还债帖。面试

两种数据类型

在JS里,分了两种数据类型,分别为基本数据类型引用数据类型算法

目前基本数据类型已有:number string boolean undefined null symbol bigint浏览器

它们的区分点在存储位置的不一样。基本数据类型是直接存储在栈内存中的,而引用数据类型则存储在堆内存里的。markdown

《天天都在写的JS判断语句,你真的了解吗?》一文中提到咱们可使用typeof运算符去识别变量的数据类型。以下图:app

在其中,typeof null === 'object'是个例外,它是惟一一个没法经过typeof识别出来的基本数据类型。函数

计算机都是以二进制方式来存储数据,JS中若是二进制前三位都是0时会断定为object类型,而null的二进制前三位刚好都是0,因此返回objectoop

除了null这个特例,咱们发现全部引用数据类型又分为了两个阵营:functionobject。它们是否有内在联系呢?post

Function和Object是好基友👬

先抛出个问题,什么是对象? 我会给出这样的答案:所谓对象,都有本身的属性和方法

这是我本身的认识,不必定正确,欢迎你评论,说出你的想法。

那么,函数是对象吗?是!由于它符合上面的定义:

function Foo(){}
Foo.name = 'Foo'
Foo.getName = function(){return Foo.name}
Foo.getName()  // Foo
复制代码

在JS中,对象是“第一等公民”,那么怎样才算“第一等公民”?

  • 能够被动态建立;
  • 能够赋值给变量;
  • 能够做为函数的入参或出参;
  • 能够包含本身的属性或方法;

显然,函数符合这四个条件,因此函数也是“第一等公民”。因此,函数即对象

事实上,在ECMA标准中,已直接将函数划归到 Sandard Built-in ECMAScript Objects

既然“函数即对象”,JS又怎样具体区分出函数与对象呢?ECMA给出了答案: 若是一个对象有内部属性[[Call]],则它是一个函数; 若是一个对象没有内部属性[[Call]],则它是一个普通对象;

函数肩负了两项职责:

  1. 逻辑函数:用于封装业务逻辑,处理事务;
  2. 构造函数:用于对象实例化,此时必须用new操做符调用函数;
function Person(name){
    this.name = name
}
let p = new Person()
typeof Person  // function
typeof p  // object
复制代码

在JS中,已经为咱们准备好不少的内置构造函数,好比Function Object Array Date RegExp等。当你使用typeof操做符识别它们,它们都会返回function

typeof Function  // function
typeof Array  // function
复制代码

你有没有发现,new Date()Date() 你均可以获得一个 Date 实例化对象,你知道怎么作到的吗?欢迎评论 。

至此咱们有了基本认知,首先函数拥有对象相同的能力(函数即对象),同时函数还能实例化对象。在Function和Object背后必定有着“隐蔽”的内在联系。

原型对象prototype

计算机执行任何逻辑都须要成本:时间成本和内存成本。一样一件事两种作法,显然咱们会选择成本更低的那种作法。一样的,JS也“不笨”。

先看以下示例代码:

let dog1 = {
    name: '旺财',
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}

let dog2 = {
    name: '大黄',
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
} 
复制代码

狗生艰难,都要背负属性species和方法bark,任重而道远。因而旺财和大黄商议,能不能有个”代理“的机制,将共性部分的属性都交给这个代理,由代理全权负责,本身只保留不一样的部分(name)。因而有了这个”代理“:

let agent = {
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}
let dog1 = {
    name: '旺财',
    __agent__: agent
}
let dog2 = {
    name: '大黄',
    __agent__: agent
}
复制代码

当访问到一个属性,本身身上没有的,就去”代理“那里找。若是找到了,皆大欢喜;未果,万分抱歉。

”代理“就如同制做陶瓷碗的模具,它记录了陶瓷碗的高度、碗口大小等,这样保证制做出每一个碗都是同种规格。而各个碗又能够涂上不一样的色彩,保留了它的个性。

在JS中,这个”代理“就是原型prototype。原型保存了同类型对象的共享属性和方法,实质上就是为了节省内存空间。从上面的示例中看到agent是一个对象,一样JS中prototype也是一个对象,即原型对象

那么,这个prototype对象应该放在哪里最合适呢?(小蝌蚪开始找妈妈了)

咱们从上一节中知道,对象能够由函数实例化,即用new操做符运行函数,至关于同类型的对象也都是由一样的函数实例化的,此时函数变身为构造函数。-->(同规格的碗必定是从一个模子里倒出来的)

另外一方面,函数即对象,那么就能够在函数上挂载一些属性。Bingo!把prototype挂载在构造函数上必定是最合适的,由于对象实例化必定会经历构造函数运行。 因而,修改一下上面的示例:

function Dog(name){
    this.name = name
}
Dog.prototype = {
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}
// Oops, 好像丢了什么关键信息?

let dog1 = new Dog('旺财')
let dog2 = new Dog('大黄')
dog1.species === dog2.species  // true
复制代码

细心的读者在这里应该会纠正我了,给Dog构造函数直接赋值prototype会丢失一些信息。哈,没错,就是contructor属性,暂且不表。先要研究在new的过程当中,是怎样操做这个原型的。

new操做符 与 [[Prototype]]

引自MDN中的定义,new运算符建立一个用户定义的对象类型的实例或具备构造函数的内置对象的实例。

管它是啥,咱们关心的是new内部的猫腻。 以let dog = new Dog('旺财')为例,new进行了以下操做:

  • Step 1:建立一个空对象 obj = {}
  • Step 2:将obj的内部属性[[Prototype]]指向构造函数Dog的原型,即obj.[[Prototype]] = Dog.prototype
  • Step 3:将构造函数Dogthis指向这个空对象obj
  • Step 4:执行构造函数Dog,函数内部对this的操做等同于操做obj对象;
  • Step 5:若是函数Dog指定了返回值,则正常返回这个返回值;不然,返回obj对象;

以上就是new操做符的黑魔法,它已经帮咱们完成了对象实例化后原型的绑定,完成了“代理”指定。 其中,提到了obj空对象的内部属性[[Prototype]],它在new过程当中会被指向构造函数的原型,至关于这个空对象obj继承了原型对象。 依据ECMA标准中的描述,每一个对象都会有这个[[Prototype]]内部属性,为的就是实现对象继承。

既然是内部属性,咱们天然没法直接经过obj.[[Prototype]]访问到。目前有两个方式能够从一个对象obj上获取到它继承的原型对象:

  1. 野路子obj.__proto__
  2. ECMA官方Object.getPrototypeOf(obj)

之因此将obj.__proto__称为野路子是由于对象上的__proto__属性是由各个浏览器本身实现的,目的是为了方便开发者调试代码,ECMA官方可不认可这种方式哦。

在ECMA2015中没有任何关于__proto__的说明,在最新的ECMA标准中你能够在附录中找到关于__proto__说明,但它是可配置的属性,便可能会被任意覆盖。

所以切记:不要在生产环境中使用__proto__,请使用Object.getPrototypeOf(obj),当且仅在开发环境中可使用__proto__。 若是你实在忍不住,也请作好字段校验 const hasProto = '__proto__' in {}

prototype vs [[Prototype]]
这里势必要把二者拿出来比较一下,毕竟用了同一个单词嘛。请记住如下两句话:

  • 只有函数才有prototype属性,prototype是一个对象(即,原型对象),对象上包含全部实例对象共享的属性或方法;
  • 全部对象都有[[Prototype]]内部属性,它指向建立该对象的构造函数的原型对象;

比较拗口,请多读几遍这两句话,理解其中含义!

依据“函数即对象”的定义,函数会同时拥有prototype[[Prototype]]属性,对象不会有prototype属性,只有[[Prototype]]属性。

至此,函数与对象果真是有“一腿”的,依靠prototype[[Prototype]]产生了“剪不断理还乱”的关系。

constructor属性

每一个对象都有构造函数!

上面提到咱们能够经过Object.getPrototyoeOf(obj)方法方便的获取到任一对象的原型对象。那么咱们也须要方便的从一个对象上面轻松获取到该对象的构造函数。

读取对象obj.constructor属性就能够得到它的构造函数,这里须要注意的是,constructor是挂载在构造函数的原型对象prototype上的。而在实例化对象上读取的constructor属性都是从它的构造函数原型上继承来的。

上节中咱们漏掉了对constructor的修正,若是粗暴的直接修改Dog函数的原型,会丢失constructor属性,访问dog1.constructor,获得的是从Object构造函数原型上继承来的constructor,显然是不正确的,须要将constructor指向正确的Dog函数上:

function Dog(name){
    this.name = name
}
Dog.prototype = {
    constructor: Dog,
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}

let dog1 = new Dog('旺财')
dog1.constructor === Dog  // true
复制代码

有个面试题:问Dog.prototype = {xxx: yyy}Dog.prototype.xxx = yyy的区别在哪? --> 前者须要修正constructor

真的须要修正constructor吗?
平心而论,我在写函数封装类的时候,常常会覆盖它的原型,而且忘记修正constructor属性,但也没有任何异常的事情发生,长此以往,不修正constructor属性成了天然。

没错,不会有什么异常的事情发生,前提是你不会显式的调用constructor

constructor指向构造函数,因此constructor是函数,能够被直接调用。 仍然是上面的示例(不修正constructor),一般而言,咱们是直接调用new Dog()去实例化对象,而不会直接调用constructor,那若是直接调用constructor呢?

let dog2 = new dog1.constructor('大黄')
dog2.name  // undefined
dog2 instanceof Dog // false
dog2 instanceof Object // true
dog2 instanceof String // true
复制代码

为何dog2String的实例,留给你去思考啦

若是不显式的调用constructor好像也没啥问题啊,那你能保证你使用的某个第三方库就不会显式的调用你传递给它的对象的constructor吗?

养成习惯,及时修正你的constructor

再谈Function 和 Object

从上节中,大体清楚了如何经过new构造函数来实例化一个对象,那么做为普通函数和普通对象的构造函数FunctionObject是怎样的关系?

首先,FunctionObject是内置构造函数,因此它们是函数,毋庸置疑:

typeof Function // function
typeof Object // function
复制代码

其次既然是函数,那么它们都有prototype对象属性,即Function.prototype Object.prototype。 而后,prototype是对象,就有[[Prototype]]内部属性,指向建立该对象的构造函数的原型对象:

Function.prototype.__proto__ === Object.prototype  // true
Object.prototype.__proto__ === Object.prototype // Oops!糟了,若是这里要是成立的话,就会陷入死循环了
复制代码

JavaScript为了避免陷入死循环,所谓“恩恩怨怨什么时候了”,总归须要一个终点,不能无休止遍历下去,所以当访问到Object.prototype.__proto__时,将强制返回null,意思是你访问到头了,没有更多内容了,洗洗睡吧。(标准中关于Object Prototype的说明

同时,咱们在标准中也获取到以下两条信息:

  1. 每一个内置函数或内置构造函数的[[Prototype]]都指向Function.prototype
  2. 每一个内置原型对象的[[Prototype]]都指向Object.prototype,除了Object.prototype自身之外;

在前面,咱们知道函数的原型应该是对象,即typeof Foo.prototype === 'object'是成立的。可是!Function.prototype倒是一个例外typeof Function.prototype === 'function',不是object

我从ECMA2015中找到了关于Function.prototype的描述:

The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined. 即 Function.prototype是函数对象 (它的内部属性[[Class]] 是 ‘Function’)

自ECMA2015如此描述后,为了兼容它,之后的全部ECMA版本也都“将错就错”了。

ECMA:你大爷终究是你大爷,哈哈

至此,能够获得Function与Object的关系图以下:

原型链

讨论了这么多,思绪有点乱了,我将全部关系者整合到一张关系图中:

从这张关系图中,咱们能够概括出几个信息点:

  1. 函数必然有prototype属性,原型是一个对象,包含全部实例对象共享的属性或方法;
  2. 不管函数仍是对象都有[[Prototype]]内部属性,指向原型对象,其中Object.prototype.[[Prototype]]指向null,以表示原型查找的终点;
  3. prototype对象上挂载了constructor属性,指向构造函数;

那么当咱们在谈论原型链的时候,究竟咱们在谈论什么?

若是你想真实看到这条“链”,那么它就是关系图中的粉色虚线框所示,可是这里原型链只是宾语,谈论原型链时,不能够丢失主语,即foo对象的原型链。直观的说,原型链就是对象间用[[Prototype]]内部属性串联起来的单向链表

因此,函数Foo的原型链是怎样的,我想你也能在关系图找到。 函数Foo的原型链能够访问到Function.prototype.callFunction.prototype.apply

在JS中,我理解的原型链实际上是一种算法,即在对象上查找属性的算法。 以运行foo.someFunc()为例:

  • 首先,查找foo对象上是否已定义someFunc方法,未果,next;
  • 取得foo.[[Prototype]],即Foo.prototype对象,查找对象上是否已定义someFunc方法,未果,next;
  • 取得Foo.prototype.[[Prototype]],即Object.prototype对象,查找对象上是否已定义someFunc方法,未果,next;
  • 取得Object.prototype.[[Prototype]],发现是null,宣告这次查找失败,抛出TypeError错误,结束这次查找,Game over。

总结

码了这么多字,无非想和你达成以下共识:

  • 函数即对象;
  • 只有函数才有prototype属性,prototype是一个对象(即,原型对象),对象上包含全部实例对象共享的属性或方法;
  • 全部对象都有[[Prototype]]内部属性,它指向建立该对象的构造函数的原型对象prototype
  • 通常而言,原型对象上都会有constructor属性,指向原型绑定的构造函数;
  • 原型链是一种在对象上查找属性的算法;

你是否有不一样的见解?欢迎评论。

预告
基于原型,JS是如何作到类继承的?ES6中,class A extends B背后藏着什么猫腻?如何经过“混入mixin”来完成多重继承?

最后

码字不易,若是:

  • 这篇文章对你有用,请不要吝啬你的小手为我点赞;
  • 有不懂或者不正确的地方,请评论,我会积极回复或勘误;
  • 指望与我一同持续学习前端技术知识,请关注我吧;
  • 转载请注明出处;

您的支持与关注,是我持续创做的最大动力!

相关文章
相关标签/搜索