前言
原型与原型链是 JavaScript 中重要的知识点。本文先介绍了 JavaScript 中继承和原型的概念,如何详细讲解了原型与原型链,最后通过演示 new 的过程加深对原型链的理解。
继承与原型
谈 JavaScript 原型与原型链就一定要谈 JavaScript 的继承原理。
JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。(摘自《你不知道的 JavaScript》)
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(__proto__)指向它的构造函数的原型对象(prototype) 。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。(摘自MDN)
JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。 当修改原型时,与之相关的对象也会继承这一改变。
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
原型与原型链
结合上面的内容,原型与原型链的关系是这样的:
- 每一个 JavaScript 对象(除
null)在创建的时候就会与之关联另一个对象,这个对象就是原型,每一个对象都会从原型“继承”属性 - 原型也是一个对象,通过
Object构造函数生成 - JavaScript 每个实例对象都有
__proto__属性来指向它的构造函数的原型对象(prototype) - 除了
Object、Function,JavaScript 内置的构造函数还有Array、RegExp、Date、Boolean、Number、String - 每个原型对象(
prototype)都有一个constructor属性指向关联的构造函数 - 原型对象(
prototype)也有__proto__属性指向“原型的原型”,这也是原型链的基础 - JavaScript 通过原型链实现了继承关系
null是原型链的最后环节
其他地方有将
__proto__称作隐式原型,将prototype称作显式原型的,没有标准翻译,为避免混乱,本文仍然使用英文命名。
person.constructor可以获取constructor其实是从原型中获取的,这也反应了原型链的机制。
__proto__并不是 ECMAScript 语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性,通过该属性方便我们访问、修改原型对象。遵循 ECMAScript 标准,[[Prototype]]才是正统,[[Prototype]]无法被直接修改、引用。从 ES6 开始, 可通过Object.getPrototypeOf()和Object.setPrototypeOf()来访问、修改原型对象。
参照上方,我们可以画出简单的实例与原型与构造函数的关系图:
以上这些内容也许难以很快消化,我们可以结合一张经典的图和代码理解一下。
我们可以将有关原型指向的关键几点拿下来:
- JavaScript 每个实例对象都有
__proto__属性来指向它的构造函数的原型对象(prototype) - 每个原型对象(
prototype)都有一个constructor属性指向关联的构造函数 - 原型也是一个对象,通过
Object构造函数生成,也有__proto__属性指向“原型的原型” null是原型链的最后环节
可以先从 function Foo() 开始看,类似前文的实例与原型与构造函数的关系图。
参照第一点,f1 和 f2 是构造函数 Foo() 创建的实例,所以它们的 __proto__ 属性指向了 Foo.prototype。同理,由构造函数 Object 创建的 o1 和 o2 的指向也类似。
function Foo(){};
let f1 = new Foo();
let f2 = new Foo();
f1.__proto__ = Foo.prototype;
f2.__proto__ = Foo.prototype;
function Object()
let o1 = new Object();
let o2 = new Object();
o1.__proto__ = Object.prototype;
o2.__proto__ = Object.prototype;
参照第二点,Foo.prototype 的 constructor 属性指向了关联构造函数 Foo。Object 和 Function 也应如此。
Foo.prototype.constructor = Foo;
Object.prototype.constructor = Object;
Function.prototype.constructor = Function;
参照第三点,Foo.prototype 的 __proto__ 属性指向了 Object.prototype。Function 也应如此。
Foo.prototype.__proto__ = Object.prototype;
Function.prototype.__proto__ = Object.prototype;
参照第四点,Object.prototype 的 __proto__ 属性指向了 null,原型链到此停止。
Object.prototype.__proto__ = null;
前面的部分应该还是很简单的,下面是最绕的。
参照第一点,函数 Foo 其实是由 Function 创建的实例,所以它的 __proto__ 属性指向了 Function.prototype。同理,Object 也是如此。而 Function 可以看作由 Function 创建的实例,所以它的 __proto__ 属性也指向了 Function.prototype!
这是很有争议的地方,
Function真的自己创建了自己吗?下面引用了文章JavaScript深入之从原型到原型链的解释。
Function作为一个内置对象,是运行前就已经存在的东西,所以根本就不会根据自己生成自己,所以就没有什么鸡生蛋蛋生鸡,就是鸡生蛋。至于为什么Function.__proto__ = Function.prototype,我认为有两种可能:一是为了保持与其他函数一致,二是就是表明一种关系而已。 简单的说,我认为:就是先有的Function,然后实现上把原型指向了Function.prototype,但是我们不能倒过来推测因为Function.__proto__ = Function.prototype,所以Function调用了自己生成了自己。
Foo.__proto__ = Function.prototype;
Object.__proto__ = Function.prototype;
Function.__proto__ = Function.prototype;
new 的过程
我们可以通过了解调用 new 的过程来加深对原型的理解。
new 的过程是这样的:
- 创建一个空对象
- 挂载原型对象,将构造函数的
prototype属性赋值给空对象的__proto__ - 重新绑定
this - 将新对象和参数传给构造器执行
- 如果构造器返回的不是对象,那么就返回第一个新对象
const _new = function() {
const obj1 = {};
const Fn = [...arguments].shift();
obj1.__proto__ = Fn.prototype;
const obj2 = Fn.apply(obj1, arguments);
return obj2 instanceof Object ? obj2 : obj1;
}
参考资料
继承与原型链 - JavaScript | MDN
JavaScript深入之从原型到原型链 · Issue #2 · mqyqingfeng/Blog
轻松理解JS 原型原型链 - 掘金
前端面试题之JavaScript篇