原型链
前言
我们都知道js语言有原型链的概念,每个对象都会有个__proto__属性,它要么是null,要么就是对另一个对象的引用,称之为原型。先看几个小例子简单了解一下什么是原型。
tip:刚接触原型链的概念时,会分不清
__proto__和[[Prototype]]的区别。其实规范里不使用__proto__属性来保存原型,而是使用隐藏的属性[[Prototype]]。不过我们使用__proto__也没有问题。因为在规范没出来前,所有的浏览器包括服务端都已经支持了__proto__的写法,所以该属性就一直被保留了下来。它俩还是有区别的,
__proto__实际是[[Prototype]]的getter/setter。__proto__属性目前来看有点过时了,现代编程语言建议使用函数Object.getPrototypeOf/Object.setPrototypeOf来取代__proto__去 get/set 原型,但是由于__proto__标记在观感上更加明显,所以我们在后面的示例中将使用它。
原型其实就是个对象
先来看一个小例子理解下原型的概念。
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal // 设置 rabbit.[[Prototype]] = animal
};
console.log(rabbit.jumps) // true
console.log(rabbit.eats) // true如上面的例子,animal对象就是rabbit对象的原型。在调用rabbit.eats时,因为rabbit对象自身并没有这个属性,所以就会顺着原型向上找,在animal对象中找到了eats属性,所以输出了true。
从上面的例子看出,原型其实就是一个对象。
原型链可以很长
原型链可以很长,比如下面这个例子
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk 是通过原型链获得的
longEar.walk(); // Animal walk
console.log(longEar.jumps); // true(从 rabbit)现在,如果从 longEar 中读取一些它不存在的内容,JavaScript 会顺着原型链一层一层往上查找(先找 rabbit ,然后 animal )。
原型链的两点注意事项
这里会有两个限制:
引用不能形成闭环。如果我们试图在一个闭环中分配
__proto__,JavaScript 会抛出错误。jslet animal = { eats: true, walk() { alert("Animal walk"); }, __proto__: animal }; /* ReferenceError: Cannot access 'animal' before initialization */__proto__的值可以是对象,也可以是null。而其他的类型都会被忽略。忽略的意思是还按之前的引用。比如,
let arr1 = new Array(),此时arr1的__proto__默认指向Array.prototypelet o1 = {},此时o1的__proto__默认指向Object.prototypejslet animal = { eats: true, walk() { alert("Animal walk"); } }; let arr1 = new Array() console.log(arr1.__proto__ === Array.prototype); // true arr1.__proto__ = 'xxxx' // 赋值的不是对象,也不是null,会被忽略 console.log(arr1.__proto__ === Array.prototype); // true arr1.__proto__ = null // 赋值为null console.log(arr1.__proto__); // undefined,这里很有意思,赋值为null,但是输出的是 undefined arr1.__proto__ = animal // 赋值为对象 console.log(arr1.__proto__); // { eats: true, walk: [Function: walk] }
__proto__的最顶部是null
原型链可以有很长很长,那它的最顶部是个啥呢?我们来看这样一个图,看懂这个图,相信会对原型链有个很清晰的认知。(图片来自:https://github.com/KieSun/Dream/issues/2)

先来弄懂下面几个问题
图中有多少个对象?
相信很多人会直接回答,有四个!f2、f1、o1、o2,只有它们四个使用typeof时得到的值是object,所以就只有它们四个是对象。
其实是不正确的,即便typeof得到的值是function,它也是一个对象,称之为函数对象(也可以叫函数、也可以叫方法,或者叫构造方法)。图中的Object和Function就是函数对象,它们是JS的内置对象。
tip:JS中对象分为两种,函数对象和一般对象。我们使用
function xx(){}声明出来的都是函数对象。声明一般对象时我们可以使用
new关键字(new声明出来的对象,我们也叫实例对象,只有构造方法才能使用new关键字来声明实例对象),比如let o1 = new Object()或者let a1 = new Array()。或者直接用
let a = {},其实这种方式等效于let a = new Object()。
JS有很多内置对象,除了图中的Object和Function,还有很多,比如Boolean、Symbol、Error、Math、JSON、Date等等。这些内置对象大部分是函数对象,还有小部分是一般对象。函数对象使用typeof得到的值为function,一般对象得到的值为object。(null是个例外,typeof得到的值也是object,这是个历史遗留问题,特殊记一下)
function Foo() { }
let f1 = new Foo()
let f2 = new Foo()
let o1 = new Object()
let o2 = new Object()
console.log(typeof f1); // object
console.log(typeof f2); // object
console.log(typeof o1); // object
console.log(typeof o2); // object
console.log(typeof Foo); // function
/* 基本对象 */
console.log(typeof Object); // function
console.log(typeof Function); // function
/* 错误对象 */
console.log(typeof Error); // function
/* 数字和日期对象 */
console.log(typeof Number); // function
console.log(typeof Date); // function
console.log(typeof Math, 'Math 不是一个函数对象,因此它是不可构造的'); // object
/* 字符串对象 */
console.log(typeof String); // function
console.log(typeof RegExp); // function
/* 可索引的集合对象 */
console.log(typeof Array); // function
/* 使用键的集合对象 */
console.log(typeof Map); // function
console.log(typeof Set); // function
/* 结构化数据 */
console.log(typeof ArrayBuffer); // function
console.log(typeof JSON, 'JSON 不是一个函数对象,因此它是不可构造的'); // object
/* 控制抽象对象 */
console.log(typeof Promise); // function
/* 反射 */
console.log(typeof Reflect, 'Reflect不是一个函数对象,因此它是不可构造的。'); // object
console.log(typeof Proxy); // function
console.log(typeof null, 'typeof null的值是object,是个历史遗留问题,特殊记一下');xxx.prototype是个啥?
知道了什么是函数对象和一般对象后,我们继续看图,又有了个疑问。图中所有对象的__proto__(原型)都指向了xxx.prorotype上面,那么这个xxx.prorotype又是个啥?
实际上每个函数对象,默认都会有个prototype属性(prototype实际就是一个对象,上面可以挂载很多方法和属性),我们一直说的原型其实就是它。所有对象的__proto__都会指向原型上,一层一层的往上指,一直指到null。每当我们在对象上找一个属性或方法时,如果自己没有,就会去原型链上找,一层一层的找,直到找到为止。
tip:函数对象的
prototype是我们声明对象时,js默认添加上的。比如
let o1 = {},对象o1的__proto__默认会指向Object.prototype,即:o1.__proto__ === Object.prototype
let o2 = new Array(),对象o2的__proto__默认会指向Array.prototype,即:o2.__proto__ === Array.prototype
分析f1、f2
我们先来分析一下图中的f1和f2两个对象:
function Foo() { }
let f1 = new Foo()
let f2 = new Foo()
console.log(f1.__proto__ === Foo.prototype); // true
console.log(f2.__proto__ === Foo.prototype); // true
console.log(f1.__proto__.__proto__ === Object.prototype); // true
console.log(f1.__proto__.__proto__.__proto__ === null); // true通过打印输出,验证了我们之前的分析是正确的。f1和f2两个对象的__proto__都指向Foo.prototype,即f1.__proto__ === Foo.prototype。(f2同理)
而又因为Foo.prototype属性本身就是一个对象,对象都会有属于它的原型(有__proto__),所有对象的原型默认都指向Object.prototype,所以Foo.prototype.__proto__ === Object.prototype,即f1.__proto__.__proto__ === Object.prototype。
那么问题来了,如果说所有的原型都是一个对象,对象的原型默认都指向Object.prototype,那Object.prototype按理说也应该是个对象,那它不就自己指自己了,这种情况不就报错了?那 Object.prototype的原型指向哪里呢?
其实我们标题已经给出了答案,没错,是null,我们已经找到头了,Object.prototype.__proto__ === null,即f1.__proto__.__proto__.__proto__ === null。
分析o1、o2
我们再来分析一下图中的o1和o2两个对象:o1、o2的分析和f1、f2一样,都是类似的,直接看运行结果。
let o1 = new Object()
let o2 = new Object()
let o3 = {} // 可以看成 let o3 = new Object()
console.log(o1.__proto__ === Object.prototype); // true
console.log(o2.__proto__ === Object.prototype); // true
console.log(o3.__proto__ === Object.prototype); // true
console.log(o1.__proto__.__proto__ === null); // true分析Foo
根据上文,我们已经知道,对象分为一般对象和函数对象,一般对象原型__proto__默认都指向Object.prototype,那么函数对象的原型默认指向哪呢?答案是,指向Function.prototype。
函数对象(也成为构造函数或函数)是使用function关键字声明出来的,图中的Foo就是一个函数对象。
function Foo() { }
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Foo.__proto__.__proto__ === Object.prototype); // true
console.log(Foo.__proto__.__proto__.__proto__ === null); // true通过打印输出,验证了我们的分析是正确的。Foo的原型指向了Function.prototype,即:Foo.__proto__ === Function.prototype。
Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Foo.__proto__.__proto__ === Object.prototype。
Object.prototype的原型指向null,即:Foo.__proto__.__proto__.__proto__ === null。
分析Function
我们已经知道函数对象的原型默认都指向Function.prototype,那么问题来了Function本身就是一个函数对象,那它的原型指向哪呢?没有错,指向它自己的prototype,即Function.__proto__ === Function.prototype,图上也是这么画的,我们举例来验证一下。
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.__proto__.__proto__ === Object.prototype); // true
console.log(Function.__proto__.__proto__.__proto__ === null); // true通过打印输出,验证了我们的分析是正确的。Function的原型指向了Function.prototype,即:Function.__proto__ === Function.prototype。
Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Function.__proto__.__proto__ === Object.prototype。
Object.prototype的原型指向null,即:Function.__proto__.__proto__.__proto__ === null。
分析Object
根据上面的一大堆分析,到现在我们已经能猜测出结果了。因为Object也是一个内置的函数对象,所以它的原型应该指向Function.prototype,然后剩下的就都一样了。图上也是这么画的,我们拿例子验证一下。
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true
console.log(Object.__proto__.__proto__.__proto__ === null); // true根据打印输出,验证我们的猜想是正确的。Object的原型指向了Function.prototype,即:Object.__proto__ === Function.prototype。
Function.prototype本身就是一个对象,所以它的原型默认指向Object.prototype,即:Object.__proto__.__proto__ === Object.prototype。
Object.prototype的原型指向null,即:Object.__proto__.__proto__.__proto__ === null。
以上,已经分析完了所有情况,其实就是这么简单。
原型实际就是一个对象,
function声明出来的函数对象(也称构造函数或函数),它的原型默认指向Function.prototype。JS内置的函数对象的原型默认也指向Function.prototype。顺着原型链往上找,最终肯定能指到Object.prototype,再往上就是null了。
constructor是个啥?
整张图我们已经分析的差不多了,但是图中还遗漏了一处,constructor是个啥?
看图我们发现,Foo.prototype.constructor又指回了Foo、Object.prototype.constructor又指回了Object、Function.prototype.constructor又指回了Function。我们自己来验证一下:
function Foo() { }
let f1 = new Foo()
console.log(Foo.prototype.constructor === Foo); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Function.prototype.constructor === Function); // true
console.log(f1.constructor === Foo); // true通过验证我们发现,确实是这么回事,原型的constructor属性又指向了函数对象(构造函数)本身。不仅是原型保存了这个函数对象(构造函数),实例对象也保存了一份,如示例中的f1.constructor === Foo。那constructor的作用是干什么的呢?
作用一:类型判断
其实,constructor就是保存了一份对象的构造方法,好让我们知道这个对象是从哪里来的。要说它真正的作用,JS底层可能会用到,以下是我的猜测:
底层用于类型判断 比如
typeof:我们在使用
function关键字声明一个函数时,其实这个函数就是一个构造函数,本文中我们也称之为函数对象,工作中我们习惯的叫法说它是个函数。其实不管怎么称呼,只要是function声明出来的,我们都可以使用new关键字来构建一个实例对象,然后实例对象就可以通过原型链,访问原型上面的方法。所以,当使用
function声明一个构造方法时,JS为它自动绑定了一个prototype对象,然后又自动的往prototype塞了一个自身的引用constructor,即xxx.prototype.constructor === xxx。只要是这种格式的对象,JS都认定它是函数对象(构造方法),可以使用new关键字来声明实例对象。使用typeof进行类型校验时,会返回function。假如我们随便定义一个
非function类型(假如是个number),我们强行对齐使用new,就会报错。比如:
let a = 1,对a强行使用new,let o1 = new a,JS会抛出一个错误,TypeError: a is not a constructor,所以我猜想保存constructor除了告诉我们该对象是从哪里来的,还一个作用就是为了用作类型判断,所有的构造函数typeof之后的值都为function。
补充个MDN给构造函数的定义:
构造函数属于被实例化的特定类对象 。构造函数初始化这个对象,并提供可以访问其私有信息的方法。构造函数的概念可以应用于大多数面向对象的编程语言。本质上,JavaScript 中的构造函数通常在类的实例中声明。(跟自己理解的差不多,构造函数本身也是一个对象。)
参考:https://developer.mozilla.org/zh-CN/docs/Glossary/Constructor
作用二:创建新的实例对象
除了上述作用,我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器。比如:
function Rabbit(name) {
this.name = name;
console.log(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");当我们有一个对象,但不知道它使用了哪个构造器(例如它来自第三方库),并且我们需要创建另一个类似的对象时,用这种方法就很方便。
使用constructor的注意事项
……JavaScript 自身并不能确保正确的 "constructor" 函数值。
它存在于函数的默认 prototype 中,但仅此而已。之后会发生什么 —— 完全取决于我们。
如果我们将整个默认 prototype 替换掉,那么其中就不会有 constructor 了,比如:
function Rabbit() {}
Rabbit.prototype = {
jumps: true
};
let rabbit = new Rabbit();
console.log(rabbit.constructor === Rabbit); // false因此,为了确保正确的 constructor,我们可以选择添加/删除属性到默认的 prototype中,而不是将其整个覆盖掉:
function Rabbit() {}
// 不要将 Rabbit.prototype 整个覆盖
// 可以向其中添加内容
Rabbit.prototype.jumps = true
// 默认的 Rabbit.prototype.constructor 被保留了下来或者,也可以手动重新创建 constructor 属性:
Rabbit.prototype = {
jumps: true,
constructor: Rabbit
};
// 这样的 constructor 也是正确的,因为我们手动添加了它总结
- 在 JavaScript 中,所有的对象都有一个隐藏的
[[Prototype]]属性,它要么是另一个对象,要么就是null。 - 通过
[[Prototype]]引用的对象被称为“原型”。 __proto__属于[[prototype]]的getter/setter。__proto__并不是语言本身的特性,是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf方法来获取实例对象的原型,然后再来为原型添加方法或属性。- 如果我们想要读取
obj的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。 - 原型的最顶部是
null。 - 通过构造方法(假设为
Foo)new出来的对象(假设为o),会默认有个constructor属性,该属性指向它的构造方法,即o.constructor ===Foo。 - 通过构造方法(假设为
Foo)new出来的对象(假设为o),它的__proto__会默认指向它的构造方法的原型上,即o.__proto__ ===Foo.prototype。 - 构造方法(假设为
Foo)的原型上默认有个constructor属性,该属性执行它自己,即Foo.prototype.constructor === Foo。
参考
https://github.com/KieSun/Dream/issues/2
https://zh.javascript.info/prototype-inheritance