开篇先放大招:
总结起来 JS 的原型链就只有两条规则:
- 构造对象 的原型指向 构造函数 的 prototype 属性。
- Object.prototype 的原型不可更改且指向 null(原型链的尽头)。
这有点类似于数学归纳法:先有一个初始条件,再有一套递归规则。接下来,我会用几个例子来证明这个观点。
JS 中最重要的三个数据结构就是对象、函数和数组。下面我将分别捋清这 3 类对象的原型链。
相信你一定看过这张图来源:www.cnblogs.com/dreamcc/p/1…
这张图虽然画得不错,但对于初学者来说,更像是一堆零碎知识点的堆叠。其实,关于原型链,核心规则就是我上面说的那两条。接下来我们来验证。
对象
一个普通对象 {} 是由 Object 这个构造函数构造出来的。根据第一条规则,构造对象的原型会指向构造函数的 prototype 属性,所以:
console.log(Object.getPrototypeOf({}) === Object.prototype); // true
而 Object 本身又是一个函数。函数都是通过 Function 构造出来的,所以:
console.log(Object.getPrototypeOf(Object) === Function.prototype); // true
Object.prototype 的值是一个对象。按理来说,所有对象都应该通过 Object 构造出来,但这里为了避免循环,规范对它做了特殊处理,让它的原型直接指向 null:
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
PS:这里补充一个知识点。
null可以理解为空对象,undefined可以理解为空值。这里会涉及JS中的原始值,不懂的同学可以去MDN上看一看。
数组
一个普通数组是由 Array 这个函数构造出来的,所以:
console.log(Object.getPrototypeOf([]) === Array.prototype); // true
和上面的 Object 一样,Array 本身也是一个函数,所以:
console.log(Object.getPrototypeOf(Array) === Function.prototype); // true
Array.prototype 是一个对象,而对象都是由 Object 构造的。这里和 Object.prototype 不一样,需要注意:
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
函数
无论是普通函数还是箭头函数,它们默认都是由 Function 这个函数构造出来的,所以:
function fnc() {
return 0;
}
const arrFnc = () => {
return 0;
};
console.log(Object.getPrototypeOf(fnc) === Function.prototype); // true
console.log(Object.getPrototypeOf(arrFnc) === Function.prototype); // true
Function.prototype 是一个对象,所以:
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // true
constructor
constructor 这部分的规则也比较简单,首先我们要知道,设置 constructor 属性的目的是什么。
假设我现在有一个对象,想知道它是如何构造出来的,那我应该怎么找?
假设有这样一种情况:
class Person {
constructor(name: string) {
this.name = name;
}
name: string = '';
}
const person = new Person('bo');
console.log(Object.getPrototypeOf(person) === Object.prototype); // false
难道我要把项目里所有的 class 都遍历一遍,才能找到它是谁构造出来的吗?显然不现实。constructor 的意义就在这里,它提供了一条可以快速回溯到构造函数的路径。
class Person {
constructor(name: string) {
this.name = name;
}
name: string = '';
}
const person = new Person('bo');
console.log(Object.getPrototypeOf(person).constructor === Person); // true
const copyPerson = Object.getPrototypeOf(person).constructor;
const child = new copyPerson('child');
console.log(child.name); // child
console.log(Object.getPrototypeOf(child) === Object.getPrototypeOf(person)); // true
总结一下,constructor 的作用就是让你可以通过原型链快速找到对应的构造函数,所以这里形成了一个回环结构。
flowchart LR
instance["instance"]
proto["Class.prototype"]
cls["Class"]
instance -->|"__proto__"| proto
proto -->|"constructor"| cls
cls -->|"prototype"| proto
cls -->|"create"| instance
了解了这四个部分之后,就能和前面那种大图一一对上了。总结来说,原型链的知识其实并不复杂。它看起来乱,是因为相关概念比较分散,但底层规则并不复杂。学习编程和学习数学一样,关键是把握规律;如果只是死记硬背,思路就会越学越乱。
补充
为了方便讲解,文章里有些地方采用了不那么严谨的说法。这里先列出一部分,如果有遗漏,也欢迎各位同学指出:
- 不是所有对象的原型都指向
Object.prototype,只有默认创建出来的对象通常是这样。比如直接字面量创建,或者仅仅调用Object()。你也可以使用Object.create在创建对象时指定原型,还可以在创建之后使用Object.setPrototypeOf更改对象原型,但这些都不违背我上面总结的那两条规则。 - 普通函数和箭头函数的区别,我会在以后的文章里再写;但在原型这一节里,你可以先把它们看作没有本质区别。
- 我提出的这两点关于原型链的总结,只是我个人当前的理解。如果有我没有覆盖到的地方,也欢迎大家积极评论,帮我找出错误,我们一起进步。