js 深度学习 -原型 [[Prototype]]

js 深度学习 -原型 [[Prototype]]

=

You-Dont-Know-JS(你不知道的 js 这本书的开源版本)

github 国内翻译

https://github.com/JoeHetfield/You-Dont-Know-JS

掘金中文

https://juejin.cn/post/6844903478813261831

github 原帖

https://github.com/getify/You-Dont-Know-JS

=

JavaScript 的[[Prototype]]机制和 类 不 一样。

[[Prototype]]机制是一个内部链接,它存在于一个对象上,这个对象引用一些其他的对象。

这种链接主要在对第一个对象进行属性/方法引用时,某些属性/方法不存在的时候触发。在这种情况下,[[Prototype]]链接告诉引擎在那个被链接的对象上能查找这个属性/方法。接下来,如果这个对象不能满足查询,它的[[Prototype]]又会被查找,如此继续。这个在对象间的一系列链接构成了所谓的“原形链

总结

当试图在一个对象上进行属性访问,而对象没有该属性时,对象内部的[[Prototype]]链接定义了[[Get]]操作下一步应当到哪里寻找它。这种对象到对象的串行链接定义了对象的“原形链”(和嵌套的作用域链有些相似),在解析属性时发挥作用。

所有普通的对象用内建的**Object.prototype**作为原形链的顶端(就像作用域查询的顶端是全局作用域),如果属性没能在链条的前面任何地方找到,属性解析就会在这里停止**toString()**,**valueOf()**,和其他几种共同工具都存在于这个**Object.prototype**对象上**这解释了语言中所有的对象是如何能够访问他们的**

在传统的面向类语言中,类实际上发生了从父类向子类,由子类向实例的 拷贝 动作,而在 JavaScript 中的关键区别是,没有拷贝发生。取而代之的是对象最终通过**[[Prototype]]**链 链接在一起

“委托”是一个更确切的术语,因为这些关系不是 拷贝 而是委托 **是****链接**

链式调用

赋值属性的遮蔽属性问题

myObject.foo = “bar”赋值的三种场景,当foo 不直接存在 myObject,但 存在 myObject[[Prototype]]链的更高层:

  1. 如果一个普通的名为foo的数据访问属性在[[Prototype]]链的高层某处被找到而且没有被标记为只读(**writable:false**),那么一个名为foo的新属性就直接添加到myObject上,形成一个 遮蔽属性。(就是一个简单的赋值,都有这么深刻…)
  2. 如果一个foo[[Prototype]]链的高层某处被找到,但是它被标记为 只读(**writable:false**) ,那么设置既存属性和在myObject上创建遮蔽属性都是 不允许 的。如果代码运行在strict mode下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽
  3. 如果一个foo[[Prototype]]链的高层某处被找到,而且它是一个 setter,那么这个 setter 总是被调用。没有foo会被添加到(也就是遮蔽在)myObject上,这个foosetter 也不会被重定义。

如果你想在第二和第三种情况中遮蔽foo,那你就不能使用=赋值,而必须使用Object.defineProperty(..))将foo添加到myObject。也就是说 Object.defineProperty() 不会被限制,只有普通的 赋值 = 才会。

例子:

++操作符相当于myObject.a = myObject.a + 1。结果就是在[[Prototype]]上进行a[[Get]]查询,从anotherObject.a得到当前的值2,将这个值递增 1,然后将值3[[Put]]赋值到myObject上的新遮蔽属性**a**上。

机制

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo(name) {
this.name = name;
}

Foo.prototype.myName = function () {
return this.name;
};

var a = new Foo("a");
var b = new Foo("b");

a.myName(); // "a"
b.myName(); // "b"

这段代码展示了另外两种“面向类”的花招:

  1. this.name = name:在每个对象(分别在ab上;参照第二章关于this绑定的内容)上添加了.name属性,和类的实例包装数据值很相似。
  2. Foo.prototype.myName = …:这也许是更有趣的技术,它在Foo.prototype对象上添加了一个属性(函数)。现在,也许让人惊奇,a.myName()可以工作。但是是如何工作的?

a 和 b 的 myName() 并不是从 Foo 上拷贝而来。而是继承了 Foo 的 prototype 链上的 myName

因为 ab都最终拥有一个内部的[[Prototype]]链接链到Foo.prototype。当无法分别在ab中找到myName时,就会在Foo.prototype上找到 ( 继承来的 )。

关于构造器

上图的例子中。实际上不是真的。a上没有.constructor属性,而a.constructor确实解析成了Foo函数,“constructor”并不像它看起来的那样实际意味着“被 XX 创建”。

在现实中,Foo不会比你的程序中的其他任何函数“更像构造器”。函数自身 不是 构造器。但是,当你在普通函数调用前面放一个new关键字时,这就将函数调用变成了“构造器调用”。事实上,new在某种意义上劫持了普通函数并将它以另一种方式调用:构建一个对象,外加这个函数要做的其他任何事

举个例子:

1
2
3
4
5
6
7
8
function NothingSpecial() {
console.log("Don't mind me!");
}

var a = new NothingSpecial();
// "Don't mind me!"

a; // {}

NothingSpecial仅仅是一个普通的函数,但当用new调用时,几乎是一种副作用,它会 构建 一个对象,并被我们赋值到a。这个 调用 是一个 构造器调用,但是NothingSpecial本身并不是一个 构造器

换句话说,在 JavaScript 中,更合适的说法是,“构造器”是在前面 用**new**关键字调用的任何函数

函数不是构造器,但使用 new被使用时,函数调用是一个“构造器调用”。

注意:

“constructor”并不像它看起来的那样实际意味着“被 XX 创建”。它只会跟着 prototype 链一直寻找,是一个没有原则且危险的家伙。

考虑这段代码:

1
2
3
4
5
6
7
8
9
10
11
function Foo() {
/* .. */
}

Foo.prototype = {
/* .. */
}; // 创建一个新的prototype对象 没有声明constructor

var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

由此可见 constructor 只是从下到上依次去找prototype 链中拥有 constructor 的绝不是字面意思的被谁所创建,一定不能被这个家伙给骗了。

实现 constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Foo() {
/* .. */
}

Foo.prototype = {
/* .. */
}; // 创建一个新的prototype对象

// 需要正确地“修复”丢失的`.construcor`
// 新对象上的属性以`Foo.prototype`的形式提供。
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: false, // constructor是不可枚举的
writable: true,
configurable: true,
value: Foo, // 使`.constructor`指向`Foo`
});

实例.constructor是极其不可靠的,在你的代码中不应依赖的不安全引用。一般来说,这样的引用应当尽量避免。


“(原型)继承”

实际上,我们已经看到了一个常被称为“原型继承”的机制如何工作:a可以“继承自”Foo.prototype,并因此可以访问myName()函数。但是我们传统的想法认为“继承”是两个“类”间的关系,而非“类”与“实例”的关系。

回想之前这幅图,它不仅展示了从对象(也就是“实例”)a1到对象Foo.prototype的委托,而且从Bar.prototypeFoo.prototype,这酷似类继承的亲自概念。酷似,除了方向,箭头表示的是委托链接,而不是拷贝操作。

这里是一段典型的创建这样的链接的“原型风格”代码:

1
2
3
4
5
6
7
8
9
10
11
12
function Foo(name) {
this.name = name;
}

Foo.prototype.myName = function () {
return this.name;
};

function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}

// 这里,我们创建一个新的Bar.prototype链接链到Foo.prototype

<font style="color:#000000;">Bar</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#981A1A;">=</font> <font style="color:#000000;">Object</font><font style="color:#333333;">.</font><font style="color:#000000;">create</font><font style="color:#333333;">(</font> <font style="color:#000000;">Foo</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#333333;">);</font>

// 注意!现在Bar.prototype.constructor不存在了,

// 如果你有依赖这个属性的习惯的话,可以被手动“修复”。

1
2
3
4
5
6
7
8
Bar.prototype.myLabel = function () {
return this.label;
};

var a = new Bar("a", "obj a");

a.myName(); // "a"
a.myLabel(); // "obj a"

重要的部分是Bar.prototype = Object.create( Foo.prototype )Object.create(..)凭空 创建 了一个“新”对象,并将这个新对象内部的[[Prototype]]链接到你指定的对象上(在这里是Foo.prototype)。

function Bar() { .. }被声明时,就像其他函数一样,拥有一个链到默认对象的.prototype链接。但是 那个 对象没有链到我们希望的Foo.prototype。所以,我们创建了一个 对象,链到我们希望的地方,并将原来的错误链接的对象扔掉。

这就是 Object.create() 所做的。创建一个新的对象,把目标的 prototype 链 接入新创建对象的 prototype 链中,并且会回收掉 原来的对象 (被赋值的对象)

注意: 这里一个常见的误解/困惑是,下面两种方法 能工作,但是他们不会如你期望的那样工作:

// 不会如你期望的那样工作!

<font style="color:#000000;">Bar</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#981A1A;">=</font> <font style="color:#000000;">Foo</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font><font style="color:#333333;">;</font>

// 会如你期望的那样工作

// 但会带有你可能不想要的副作用 :(

<font style="color:#000000;">Bar</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#981A1A;">=</font> <font style="color:#770088;">new</font> <font style="color:#000000;">Foo</font><font style="color:#333333;">();</font>

Bar.prototype = Foo.prototype不会创建新对象让Bar.prototype链接。它只是让Bar.prototype成为Foo.prototype的另一个引用,将Bar直接链到Foo链着的 同一个对象Foo.prototype。这意味着当你开始赋值时,比如Bar.prototype.myLabel = …,你修改的 不是一个分离的对象 而是那个被分享的Foo.prototype对象本身,它将影响到所有链接到Foo.prototype的对象。这几乎可以确定不是你想要的。如果这正是你想要的,那么你根本就不需要Bar,你应当仅使用Foo来使你的代码更简单。

Bar.prototype = new Foo()确实 创建了一个新的对象,这个新对象也的确链接到了我们希望的Foo.prototype。但是,它是用Foo(..)“构造器调用”来这样做的。如果这个函数有任何副作用(比如 logging,改变状态,注册其他对象,向**this**添加数据属性,等等),这些副作用就会在链接时发生(而且很可能是对错误的对象!),而不是像可能希望的那样,仅最终在Bar()的“后裔”被创建时发生。

于是,我们剩下的选择就是使用Object.create(..)来制造一个新对象,这个对象被正确地链接,而且没有调用Foo(..)时所产生的副作用。一个轻微的缺点是,我们不得不创建新对象,并把旧的扔掉,而不是修改默认的既存对象。

==> 让我们一对一地比较 ES6 之前和 ES6 标准的技术如何处理将Bar.prototype链接至Foo.prototype

// ES6 以前

// 扔掉默认既存的Bar.prototype

<font style="color:#000000;">Bar</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#981A1A;">=</font> <font style="color:#000000;">Object</font><font style="color:#333333;">.</font><font style="color:#000000;">create</font><font style="color:#333333;">(</font> <font style="color:#000000;">Foo</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#333333;">);</font>

// ES6+

// 修改既存的Bar.prototype

<font style="color:#000000;">Object</font><font style="color:#333333;">.</font><font style="color:#000000;">setPrototypeOf</font><font style="color:#333333;">(</font> <font style="color:#000000;">Bar</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font><font style="color:#333333;">,</font> <font style="color:#000000;">Foo</font><font style="color:#333333;">.</font><font style="color:#000000;">prototype</font> <font style="color:#333333;">);</font>

如果忽略Object.create(..)方式在性能上的轻微劣势(扔掉一个对象,然后被回收),其实它相对短一些而且可能比 ES6+的方式更易读。但两种方式可能都只是语法表面现象。其实差不多。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!