JS 原型与原型链

323 阅读5分钟

前言

原型与原型链是 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
  • 除了 ObjectFunction,JavaScript 内置的构造函数还有 ArrayRegExpDateBooleanNumberString
  • 每个原型对象(prototype)都有一个 constructor 属性指向关联的构造函数
  • 原型对象(prototype)也有 __proto__ 属性指向“原型的原型”,这也是原型链的基础
  • JavaScript 通过原型链实现了继承关系
  • null 是原型链的最后环节

其他地方有将 __proto__ 称作隐式原型,将 prototype 称作显式原型的,没有标准翻译,为避免混乱,本文仍然使用英文命名。

person.constructor 可以获取 constructor 其实是从原型中获取的,这也反应了原型链的机制。

__proto__ 并不是 ECMAScript 语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性,通过该属性方便我们访问、修改原型对象。遵循 ECMAScript 标准, [[Prototype]] 才是正统, [[Prototype]] 无法被直接修改、引用。从 ES6 开始, 可通过 Object.getPrototypeOf()Object.setPrototypeOf() 来访问、修改原型对象。

参照上方,我们可以画出简单的实例与原型与构造函数的关系图:

实例与原型与构造函数的关系图

以上这些内容也许难以很快消化,我们可以结合一张经典的图和代码理解一下。

原型链

我们可以将有关原型指向的关键几点拿下来:

  1. JavaScript 每个实例对象都有 __proto__ 属性来指向它的构造函数的原型对象(prototype
  2. 每个原型对象(prototype)都有一个 constructor 属性指向关联的构造函数
  3. 原型也是一个对象,通过 Object 构造函数生成,也有 __proto__ 属性指向“原型的原型”
  4. null 是原型链的最后环节

可以先从 function Foo() 开始看,类似前文的实例与原型与构造函数的关系图。

参照第一点,f1f2 是构造函数 Foo() 创建的实例,所以它们的 __proto__ 属性指向了 Foo.prototype。同理,由构造函数 Object 创建的 o1o2 的指向也类似。

 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.prototypeconstructor 属性指向了关联构造函数 FooObjectFunction 也应如此。

 Foo.prototype.constructor = Foo;
 Object.prototype.constructor = Object;
 Function.prototype.constructor = Function;

参照第三点,Foo.prototype__proto__ 属性指向了 Object.prototypeFunction 也应如此。

 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篇