原型链
前言
我们都知道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.prototype
let o1 = {}
,此时o1
的__proto__
默认指向Object.prototype
jslet 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