原型链和继承的本质是链表

原型链和继承的本质是链表

写在前面

原型和原型链一直都是 JavaScript 中很重要的概念,理解它们有助于我们理解预定义引用类型间的关系以及 JavaScript 中对象继承的实现机制,下面是我对原型和原型链的理解和总结。

原型链 [[prototype]]

[[ prototype ]] 的意义:正常情况下访问对象属性的时候会触发 [[ get ]] 操作,第一步会检查对象本身是否有这个属性,如果对象本身没有,则会访问对象的 [[ prototype ]]

Javascript 的继承是什么?

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。

委托这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

大多数情况下,我们把通过 new 的函数调用成为“构造函数调用”

而实际上,这只是 new 操作符的副作用 ——new 操作符将生成的实例的 [[ prototype ]] 指向函数的 prototype,再因为 JS 中访问属性的 [[ get ]] 的遍历原型链的行为导致实例可以“访问”构造函数的属性

1
2
let a = new Foo()
// new Foo() 会生成一个新对象(我们称之为 a),这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象,即 a.__proto__ === Foo.prototype

JS 继承的本质是链表的遍历过程

我们来看一个例子

1
2
3
4
5
6
7
function Super(){};
function Middle(){};
function Sub(){};

Middle.prototype = new Super();
Sub.prototype = new Middle();
var suber = new Sub();

来看下他们之间的关系

他们的关系就好像一条链表,当我们访问实例对象 suber 上的属性时,就是在这条链表上进行遍历。

面向委托的设计

以下这段代码中,我们抛弃了传统的 new、构造函数、原型这些带有迷惑性的行为。

进而使用对象委托的方式,直接把 b1 委托到 Bar,将 Bar 委托到 Foo,更贴近原型链的本质

通过比较可以看出,面向委托(对象关联)的代码显然更加简洁,因为这种代码只关注一件事:对象之间的关联关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Foo = {
init: function (who) {
this.me = who
},
identify: function () {
return "I am " + this.me
},
}
// 将 Bar 委托 Foo
Bar = Object.create(Foo)
Bar.speak = function () {
alert("Hello, " + this.identify() + ".")
}

// 将 b1 委托 Bar
var b1 = Object.create(Bar)
b1.init("b1") // 访问 Foo.init
b1.speak() // 访问 Bar.speak