js 深度学习 -原型 [[Prototype]]
js 深度学习 -原型 [[Prototype]]
=
You-Dont-Know-JS(你不知道的 js 这本书的开源版本)
github 国内翻译
https://github.com/JoeHetfield/You-Dont-Know-JS
掘金中文
https://juejin.cn/post/6844903478813261831
github 原帖
=
–
JavaScript 的[[Prototype]]机制和 类 不 一样。
[[Prototype]]机制是一个内部链接,它存在于一个对象上,这个对象引用一些其他的对象。
这种链接主要在对第一个对象进行属性/方法引用时,某些属性/方法不存在的时候触发。在这种情况下,[[Prototype]]链接告诉引擎在那个被链接的对象上能查找这个属性/方法。接下来,如果这个对象不能满足查询,它的[[Prototype]]又会被查找,如此继续。这个在对象间的一系列链接构成了所谓的“原形链“
总结
当试图在一个对象上进行属性访问,而对象没有该属性时,对象内部的[[Prototype]]链接定义了[[Get]]操作下一步应当到哪里寻找它。这种对象到对象的串行链接定义了对象的“原形链”(和嵌套的作用域链有些相似),在解析属性时发挥作用。
所有普通的对象用内建的**Object.prototype**作为原形链的顶端(就像作用域查询的顶端是全局作用域),如果属性没能在链条的前面任何地方找到,属性解析就会在这里停止。**toString()**,**valueOf()**,和其他几种共同工具都存在于这个**Object.prototype**对象上,**这解释了语言中所有的对象是如何能够访问他们的**。
在传统的面向类语言中,类实际上发生了从父类向子类,由子类向实例的 拷贝 动作,而在 JavaScript 中的关键区别是,没有拷贝发生。取而代之的是对象最终通过**[[Prototype]]**链 链接在一起。
“委托”是一个更确切的术语,因为这些关系不是 拷贝 而是委托 **是****链接**。
链式调用
赋值属性的遮蔽属性问题
myObject.foo = “bar”赋值的三种场景,当foo 不直接存在 于myObject,但 存在 于myObject的[[Prototype]]链的更高层:
- 如果一个普通的名为foo的数据访问属性在[[Prototype]]链的高层某处被找到,而且没有被标记为只读(**writable:false**),那么一个名为foo的新属性就直接添加到myObject上,形成一个 遮蔽属性。(就是一个简单的赋值,都有这么深刻…)
- 如果一个foo在[[Prototype]]链的高层某处被找到,但是它被标记为 只读(**writable:false**) ,那么设置既存属性和在myObject上创建遮蔽属性都是 不允许 的。如果代码运行在strict mode下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽。
- 如果一个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 |
|
这段代码展示了另外两种“面向类”的花招:
- this.name = name:在每个对象(分别在a和b上;参照第二章关于this绑定的内容)上添加了.name属性,和类的实例包装数据值很相似。
- Foo.prototype.myName = …:这也许是更有趣的技术,它在Foo.prototype对象上添加了一个属性(函数)。现在,也许让人惊奇,a.myName()可以工作。但是是如何工作的?
a 和 b 的 myName() 并不是从 Foo 上拷贝而来。而是继承了 Foo 的 prototype 链上的 myName
因为 a和b都最终拥有一个内部的[[Prototype]]链接链到Foo.prototype。当无法分别在a和b中找到myName时,就会在Foo.prototype上找到 ( 继承来的 )。
关于构造器
上图的例子中。实际上不是真的。a上没有.constructor属性,而a.constructor确实解析成了Foo函数,“constructor”并不像它看起来的那样实际意味着“被 XX 创建”。
在现实中,Foo不会比你的程序中的其他任何函数“更像构造器”。函数自身 不是 构造器。但是,当你在普通函数调用前面放一个new关键字时,这就将函数调用变成了“构造器调用”。事实上,new在某种意义上劫持了普通函数并将它以另一种方式调用:构建一个对象,外加这个函数要做的其他任何事。
举个例子:
1 |
|
NothingSpecial仅仅是一个普通的函数,但当用new调用时,几乎是一种副作用,它会 构建 一个对象,并被我们赋值到a。这个 调用 是一个 构造器调用,但是NothingSpecial本身并不是一个 构造器。
换句话说,在 JavaScript 中,更合适的说法是,“构造器”是在前面 用**new**关键字调用的任何函数。
函数不是构造器,但使用 new被使用时,函数调用是一个“构造器调用”。
注意:
“constructor”并不像它看起来的那样实际意味着“被 XX 创建”。它只会跟着 prototype 链一直寻找,是一个没有原则且危险的家伙。
考虑这段代码:
1 |
|
由此可见 constructor 只是从下到上依次去找prototype 链中拥有 constructor 的绝不是字面意思的被谁所创建,一定不能被这个家伙给骗了。
实现 constructor
1 |
|
实例.constructor是极其不可靠的,在你的代码中不应依赖的不安全引用。一般来说,这样的引用应当尽量避免。
“(原型)继承”
实际上,我们已经看到了一个常被称为“原型继承”的机制如何工作:a可以“继承自”Foo.prototype,并因此可以访问myName()函数。但是我们传统的想法认为“继承”是两个“类”间的关系,而非“类”与“实例”的关系。
回想之前这幅图,它不仅展示了从对象(也就是“实例”)a1到对象Foo.prototype的委托,而且从Bar.prototype到Foo.prototype,这酷似类继承的亲自概念。酷似,除了方向,箭头表示的是委托链接,而不是拷贝操作。
这里是一段典型的创建这样的链接的“原型风格”代码:
1 |
|
// 这里,我们创建一个新的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 |
|
重要的部分是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 协议 ,转载请注明出处!