[[prototype]]

JavaScript 中几乎所有属性都有一个 [[Prototype]] 内置属性,其实就是一个对于其他对象的引用。几乎所有的对象都在创建时 [[Prototype]] 属性都会被赋予一个非空的值。

JavaScript 中所有的对象都存在一个 [[Prototype]] 属性,而且它的 [[Prototype]] 是非空的。

注意:我们很快就可以看到,对象的 [[Prototype]] 链接可以为空,虽然很少见。

[[Prototype]] 一般是不为空的,但是可以人工的置为空。

思考下面的代码:

var myObj = { a: 2 } myObj.a // 2

这段代码通过字面量的方式定义了一个对象,然后访问到了这个对象的属性。作者在这里拿出这个例子来,是想说明一般是不会访问到 [[Prototype]] 属性的。

[[Prototype]] 引用有什么用呢?我们以前说过,当你试图访问一个对象的属性的时候会触发 [[Get]] 操作,比如 myObj.a。对于默认的 [[Get]] 操作来说,第一步是检查对象本身是否具有这个属性,如果有的话就使用它。

这里的知识点是关于访问对象属性的时候,会触发对象的 [[Get]] 操作,这个方法是隐藏的。

但是如果 a 不在 myObj 中,就需要使用对象的 [[Prototype]] 链了。

对于默认的 [[Get]] 操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的 [[Prototype]] 链。

这里提到了 [[Prototype]] 链,以及查找对象属性的方式,当对象本身并不具有某个属性的时候,就开始在 [[Prototype]] 链上查找。

var anotherObj = { a: 2 } var myObj = Object.create( anotherObj ) myObj.a //2

稍后我们会介绍 Object.create 的原理,现在只需要知道它会创建一个对象并把这个对象的 [[Prototype]] 关联到指定对象上。

这段代码通过关联原型的方式,来进行属性的访问,可以看到 myObj 并没有指定任何属性,此时 a 属性是访问的原型链上的 anotherObj 的 a 属性。

现在 myObj 对象的 [[Prototype]] 关联到 anotherObject。显然 myObj.a 并不存在,但是尽管如此,属性访问仍然成功地找到了值 2。

但是,如果 anotherObj 中什么也不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找下去。

这个过程会持续找到匹配的属性名或者查找完整条 [[Prototype]] 链。如果是后者的话, [[Get]] 操作的返回值是 undefined。

使用 for in 遍历对象时原理和查找 [[Prototype]] 链类似,任何通过原型链访问到的属性都会被枚举。使用 in 操作符来检查属性在对象中是否存在时,同样会查找对象整条原型链,无论属性是否可枚举

in 操作符作用于整条原型链。并且原型链的查找会在找到时就停止,找不到的话就持续到原型链的最后一层。

in 操作符用来确定当前字符串的属性名是否存在于指定对象的原型链条上。

var anotherObj = { a: 2, b: 2 } var myObj = Object.create( anotherObj ) myObj.c =12 for(var k in myObj){ console.log('found:' k) } // 'found:c' // 'found:a' // 'found:b' ('a' in myObj); // true console.log(myObj.d) // undefined

这里的代码是分别表现了原型绑定,in 操作符,查找不到的会最后输出 undefined。

因此,当你通过各种语法进行属性查找时都会查找 [[Prototype]] 链,直到找到属性或者查找完整条原型条。

Object.prototype

但是到哪里事是 [[Prototype]] 的「尽头」呢?

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的「普通」对象都「源于」这个 Object.prototype 对象,所以它包含了 JavaScript 中许多通用的功能。

所有的原型链的最外层都是 Object.prototye,这是 JavaScript 对象的基本功能。

有些功能你应该已经很熟悉了,比如说 toString 和 valueOf,以及之前介绍的 hasOwnPrototype。稍后我们还会介绍 isPrototypeOf,这个你可能不太熟悉。

属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。现在我们完整地讲解一下这个过程。

myObj.foo = 'bar'

如果 myObj 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有属性值。

如果 foo 不直接存在于原型链的上层,赋值语句 myObj.foo = 'bar' 行为就会有些不同,而且可能出乎意料。

如果属性名 foo 既出现走 myObj 中也出现在 myObj 的 [[Prototype]] 链上层,那么就会发生屏蔽。myObj 中包含的 foo 属性会屏蔽原型链上层所有的 foo 属性,因为 myObj.foo 总会选择原型链中最底层的 foo 属性。

原型链有就近原则,当在附近找到时,就不会继续往上寻找。

这里的屏蔽一词的意思是指:不再继续查找。

屏蔽比我们想象的要复杂,下面我们分析一波如果 foo 不直接存在于 myObj 中而是存在原型链上层时 myObj.foo = 'bar' 的三种情况。

  1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性,并且没有标记为只读,那就会直接在 myObj 中添加一个名为 foo 的新属性,它是屏蔽属性。
  2. 如果在 [[Prototype]] 上层存在 foo,但是它被标记为只读,那么无法修改已有属性,或者在 myObj 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在 [[Prototype]] 链上存在 foo,并且它是一个 setter,那就一定会调用这个 setter,foo 不会被添加到 myObj,也不会重新定义 foo 这个 setter。

大多数开发者都认为如果想 [[Prototype]] 链上层已经存在的属性 [[Put]] 赋值,就一定会触发屏蔽,但是如你所见,三种情况中只有第一种是这样的。

屏蔽属性是指存在于属性存在于对象本身,而不存在于整个原型链上。

在我看来,这里的三种情况,只有第一中是正常情况,第二种碰上的可能微乎其微,至于第三种,更是违反常规操作,开发这些年,从未见过不使用默认的 [[Get]] 和 [[Put]] 的,它只是为了表示知识的完整性,而深究于学院派的理论中。

至于作者为什么要在这么讲这些看似没用,其实也没用的知识,可能只是基于学院派的理论学习。但在恰恰就是目前互联网所需要的,作为一个日渐成熟的产业,这些讨论会被提出来,也是很正常的事情。

如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用

var anotherObj = { a: 2, b: 2 } var myObj = Object.create( anotherObj ) myObj.c =12 console.log( myObj) // { c: 12, ___proto___: { a: 2, b: 2 } } myObj.a = 100 console.log(myObj) // { c: 12, a: 100, ___proto___: { a: 100, b: 2 } }

var anotherObj = { a: 2, b: 2 } Object.defineProperty(anotherObj, 'foo', { value: 1000, writable: false }) var myObj = Object.create( anotherObj ) myObj.foo = 100 console.log(myObj) // { ___proto___: { a: 2, b: 2, foo: 1000 } } Object.defineProperty(myObj, 'foo', { value: 1001, writable: true }) console.log(myObj) // { foo: 1001, ___proto___: { a: 2, b: 2, foo: 1001 } }

我在这里针对第一种情况,和第二种情况做了个案例,情况确实如书中所讲的那样。

如果 defineProperty 定义了原型中的属性,并且可写与修改设置 为 false,那么直接用 = 赋值不起作用,使用 defineProperty 才会起作用。但是事实上有谁会在代码里使用 defineProperty 来定义属性呢?究竟是在什么情况下,我才会去想想配置 writable configuable enmunable 这些参数呢?

很少会用上。

「类」

现在你可能很好奇:为什么一个对象需要关联到另一个对象呢?这样做有什么好处?这个问题非常好,但是在回答之前我们首先要理解 [[Prototype]] 不是什么?

为什么一个对象需要关联到另一个对象?这个问题我也想知道,因为就我目前的项目经验,用到原型的次数寥寥无几,好几次还是我强行加进去的。

在没有类以前,人们会把方法绑定在原型对象上,来达到节省内存的地步。

作者在这里自问自答,自肯定的写作手法,多少让人忍俊不禁,这个问题非常好?你自己提的吧!

以前我们说过,JavaScript 和面向类的语法不同,它并没有作为对象的抽象模式或者说蓝图。JavaScript 中只有对象。

这句话很重要,JavaScript 中只有对象。连数组都是个伪数组,是由对象假装而成的。函数也是一个对象。

基本类型连 null 都是一个对象。

JavaScript 的基本类型 string nmber boolean undefined 不是对象。

JavaScript 这门语言设计出来,本身就是作为脚本语言,是为了在一页中使用的,如今会去讨论设计蓝图的问题,也是因为它现在扮演着更为重要的角色,代码量也是极剧增加。

「类」函数

多年以来,JavaScript 中有一种奇怪的行为一直在被无耻的滥用,那就是模仿类。我们会仔细分析这种方法。

这种奇怪的「类似类」的行为利用了函数的一种特殊属性:所有函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象:

function Foo() { } Foo.prototype // {}

这个对象通常被称为 Foo 的原型,因为我们通过名为 Foo.prototype 的属性引用来访问它。然而不幸的是,这个术语对我们造成极大的误导,稍后我们就会看到。如果是我的话就会叫它「之前被称为 Foo 的原型的那个对象。」

这个对象究竟是什么?

最直接的解释就是,这个对象是在调用 new Foo() 时创建的,最后会被关联到这个 「Foo 点 prototype」对象上。

我不明白它这句话的意思,这个对象是在调用 new Foo() 时创建的?

这似乎是说当在只有当调用 new 的时候,才会创建 prototype ,但是这个函数 Foo 被创建的时候,Foo.prototype 就已经存在了。

最后被关联到 foo 的 prototype 对象,这句话的意思是 new Foo() 创建的对象,和 Foo.prototype 是同一个对象吗?

这里的意思很不清晰,应该是翻译的问题。

function Foo() {} Foo.prototype = {a:121} var a = new Foo() // 获取 a 的原型对象 Object.getPrototypeOf( a ) // { a: 121 } a // { ___proto___: { a: 121 } }

似乎,a 的原型和 Foo 的原型对象是同一个。

调用 new Foo() 时会创建 a,其中的一步就是给 a 一个内部的 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象。

暂停一下,仔细思考这条语句的含义。

给 a 一个内部 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象?

为什么要说的这么拗口?

直接说明: a 的原型绑定在 Foo 的原型上,它不简单明了吗?

这该死的翻译,傻逼!

在面向类的语言中,类可以被复制多次,就像用模具制作东西一样。我们以前看到过,之所以会这样是因为:实例化一个类就意味着「把类行为复制到物理对象中」,对于每一个实例都会重复这个过程。

实例化是对蓝图的实体化。妈的,你个傻逼翻译,写你妈呢?

但是在 JavaScript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们 [[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象并不会完全失去联系。它们是互相关联的。

JavaScript 中并不存在真正的类,只有对象。相关的原型将这些对象联系在一起。

new Foo() 会生成一个新对象,这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象。

这就是这所有小节的唯一论点,妈的,难道不应该直接抛出来吗?为什么要在中间写出来。。

最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从「类」中复制任何行为到一个对象中,只是让两个对象相互关联。

类的数据结构是一个栈,而实例化是一个复制拷贝的过程。

JavaScript 中并没有 栈 这个结构,而复制拷贝是对这个结构的模仿。

所以,JavaScript 中没有真正的类。只是对类行为的模仿。

实际上,绝大多数 JavaScript 开发人员都不知道的秘密是:new Foo() 这个函数调用实际上并没有直接关联,这个关联是一个意外的副作用。 new Foo() 只是间接完成了我们的目标:一个关联到其他对象的新对象。

一个关联到其他对象的新对象?

这句话是似乎是重点,但是你他妈在说尼玛呢?

一个新对象?关联其他对象?

这里的一个对象是指新创建的对象,这个对象的创建通过 new 创建,关联到其他对象也是 new 关键字的功能,将新对象和 Foo 对象进行绑定。

那么有没有更直接的方法来做到这一点呢?当然!功臣就是 Object.create,不过我们现在暂时不介绍它。

那人家都说不介绍了,咱就继续往下看呗。

关于名称

在 JavaScript 中,我们并不会将一个对象复制到另一个对象,只是将它们关联起来。从视觉角度来说, [[Prototype]] 机制如下图所示,箭头从右到左,从下往上:

prototype定义自己的方法的作用(Prototype)(1)

这个机制通常被称为「原型继承」,它常常被视为动态语言版本的类继承。这个名称主要是为了对应面向类的世界中「继承」的意义,但是违背了动态脚本对应的语义。

照字面意思,似乎继承这个词语,在 JavaScript 中并不准确。

「继承」这个次会让人产生非常强的心理预期。仅仅在前面加上「原型」并不能区分出 JavaScript 中和类继承几乎完全相反的行为,因此在 20 年中造成了极大的误解。

什么叫完全相反的行为?看来不是并不准确,而是根本没有继承。

在我看来,在「继承」前面加上「原型」对于事实的曲解就好像是一只手拿橘子一只手拿苹果,然后把苹果叫做「红橘子」一样。无论添加什么标签都无法改变事实:一种水果是苹果,而另一种是橘子。

根据上面的资料,JavaScript 中仅仅是将原型绑定在同一对象,并没有继承。

更好的方法是直接把苹果叫做苹果——使用更准确并且直接的术语。这样有助于理解它们的相似之处以及不同之处,因为我们大家都明白「苹果」的含义。

什么啊?继承和原型完全不是一码事的比喻吗?

因此我认为这个容易混淆的组合术语「原型继承」严重影响了大家对于 JavaScript 机制真实原理的理解。

原型继承这个词语是说前面的红橘子一样,不伦不类。

继承意味着复制操作,JavaScript 默认并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。委托这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

这是重点,JavaScript 不存在对象的复制,所以继承无从谈起。

JavaScript 是通过委托的方式将两个对象关联在一起。这是 JavaScript 中原型的机制。

还有个偶尔会用到的 JavaScript 术语差异继承。基本原则是在描述对象行为时,使用其不同于普遍描述的特质。举例来说,描述汽车时你会说汽车是四个轮子的一种交通工具,但是你不会重复描述交通工具具备的通用特性。

如果你把 JavaScript 中对象的所有委托行为都归结到对象本身,并且把对象看作是实物的话,那就可以理解差异继承了。

但是和原型继承一样,差异继承更多的是你脑中构建出的模型,而非真实情况。它忽略了一个事实,那就是对象 B 实际上并不是被差异构造出来的。我们只是定义了 B 的一些指定特性,其他没有定义的东西都变成了「洞」。而这些洞最终会被委托填满。

默认情况下,对象并不会像差异继承暗示的那样通过复制生成。因此,差异继承也不适合用来描述 JavaScript 的 [[Prototype]] 机制。

当然,如果你喜欢,完全可以使用继承这个术语,但是无论如何它只适用于你脑中的模型,并不符合引擎的真实行为。

脑中的行为,而非真实的行为。

「构造函数」

回到之前的代码:

function Foo() { } var a = new Foo();

到底是什么让我们认为 Foo 是一个「类」呢?

其中一个原因是我们看到了关键字 new,在面向对象类的语言中构造类实例时也会用到它。另一个原因是,看起来我们执行了类构造函数方法,Foo() 的调用方式很像初始化类构造函数的调用方式。

使用了关键字 new,以及类似于构造函数一样的调用。让 Foo 看起来像是一个类。

令人迷惑的「构造函数」语义外,Foo.prototype 还有另一个绝招。思考下面的代码:

function Foo() { } Foo.prototype.constructor === Foo // true var a = new Foo() a.constructor === Foo // true

Foo.prototype 默认有一个公认并且不可枚举的属性 constructor,这个属性引用的是对象关联函数,本例中是 Foo。此外,我们可以看到通过「构造函数」调用 new Foo() 创建的对象也有一个 construotor 属性

Foo.prototype.constructor 不可枚举,引用的是对象关联对象,也据是原型前面的对象,也就是自身。

按照 JavaScript 世界的惯例,「类」名首字母要大写,所以名字写作 Foo 而非 foo 似乎也提示它是一个「类」。显而易见,是吧?

构造函数还是调用

上一段代码很容易让人认为 Foo 是一个构造函数,因为我们使用 new 来调用它并且看到它的「构造」了一个对象。

实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数前面加上 new 关联字之后,就会把这个函数变成一个「构造函数调用」。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。

Foo 函数只是一个普通的函数,特殊之处在于 new 关键字的行为。

new 关键字会以构造对象的形式来调用函数。

function Nothing() { console.log('i feel busy') } var a = new Nothing() // 'i feel busy' a // Nothing { ___proto___: { constructor: ƒ Nothing() } }

Nothing 只是一个普通的函数,但是使用 new 调用时,它就会构造一个对象并赋值给 a,这看起来是 new 的一个副作用。这个调用是一个构造函数调用,但是 Nothing 本身并不是一个构造函数。

这里提到了一个概念:构造函数调用。

在 JavaScript 中并没有构造函数,只有以构造函数方式调用。

换句话说,在 JavaScript 中对于「构造函数」最准确的解释是,所有带 new 的函数调用。

函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成「构造函数调用」。

技术

我们是不是已经介绍了 JavaScript 中所有和「类」相关的问题了呢?

不是,JavaScript 开发者绞尽脑汁想要模仿类:

function Foo(name){ this.name = name } Foo.prototype.myName = function() { return this.name } var a = new Foo('foo') var b = new Foo('bar') a.name // 'foo' b.name // 'bar' a // Foo { name: 'foo', ___proto___: { constructor: ƒ Foo(), myName: ƒ () } }

这段代码展示了另外两种「面向类」的技巧:

  1. this.name = name 给每个对象都添加了 name 属性,有点像类实例的封装的数据值。
  2. Foo.prototype.myName = .. 可能是个有趣的技巧,它会给 Foo.prototype 对象添加一个属性。现在, a.name() 可以正常的工作,但是你可能会觉得惊讶,这是什么原理呢?

在这段代码中,看起来似乎创建了 a 和 b 时会把 Foo.prototype 对象复制到这两个对象中,然而事实是不是这样呢?

在本章开头介绍默认 [[Get]] 算法时我们介绍过 [[Prototype]] 链,以及当属性不直接存在于对象时如何通过它来进行查找。

因此,在创建的过程当中,a 和 b 的内部 [[Prototype]] 都将会关联到 Foo.prototype 上。当 a 和 b 中无法找到 myName 时,它会通过在 Foo.prototype 上找到。

Foo.prototype.myName 中的 myName 属性并没有绑定在 a 和 b 对象上,而是绑定在 Foo 的原型对象上,而 Foo 的原型就是 a 和 b 的原型,这个指定的过程是通过 new 关键字来完成的,new 关键字将 a、b 与 Foo.prototype 连接起来。

回顾「构造函数」

之前讨论 constructor 属性时我们说过,看起来 a.constructor === Foo 为 true 意味着 a 确实有一个指向 Foo 的 constructor 属性,但是事实不是这样。

这是一个很不幸的误解,实际上,constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo。

这似乎说的是 Foo.prototype.constructor 默认指向 Foo, 但是在调用 new 之后,就变了?

这段话似乎和下段文字是相违背的,妈的,傻逼翻译,草拟妈!

constructor 说的是 a.constructor 同样被委托给了 Foo.prototype

function Foo(name){ this.name = name } Foo.prototype.myName = function() { return this.name } var a = new Foo('foo') var b = new Foo('bar') a.name // 'foo' b.name // 'bar' a // Foo { name: 'foo', ___proto___: { constructor: ƒ Foo(), myName: ƒ () } } var c = new a.constructor('2121') c // Foo { name: '2121', ___proto___: { constructor: ƒ Foo(), myName: ƒ () } }

把 constructor 属性指向 Foo 看作是 a 对象由 Foo 「构造」非常容易理解。但这只不过是一种虚假的安全感。 a.constructor 只是默认的 [[Prototype]] 委托指向 Foo,这和「构造」毫无关系。相反,对于 constructor 的错误理解很容易对你自己产生误导。

a.constructor 不是构造,而是将 [[Prototype]] 委托指向 Foo

举例来说,Foo.prototype 的 constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 prototype 对象引用,那么新对象并不会自动获得 constructor 属性。

function Foo(name) { this.name = name } Foo.prototype = {} var a = new Foo(2121) a // { name: 2121 } a.constructor === Foo // false a.constructor // ƒ Object()

Object() 并没有「构造」 a,对吧?看起来应该是 Foo() 「构造」了它。大部分开发者都认为是 Foo() 执行了构造工作,但是问题在于,如果你认为「constructor」表示「由……构造」的话,a.constructor 应该是 Foo,但是它不是 Foo!

到底怎么回事? a 并没有 constructor 属性,所以它会委托 [[Prototype]] 链上的 Foo.prototype。但是这个对象也没有 constructor 属性(不过默认的 Foo.prototype 对象有这个属性,默认的已经被修改为空对象)。所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象有 constructor 属性,指向内置的 Object 函数。

Foo.prototype 被人为修改成空对象。所以指向 Object.prototype,Object.prototype.constructor 指向 Obeject

错误观点已经被摧毁。

这里的错误观点指的是 a.constructor === Foo 理解为由 Foo 构造,然而并不是。

a 的原型就是 Foo 的原型。a.constructor 即 Foo.prototype.constructor

当然,你可以给 Foo.prototype 添加一个 consructor 属性,不过这需要手动添加一个符合正常行为的不可枚举属性。

举例来说:

function Foo() {} Foo.prototype = {} // 创建一个新原型对象 // 需要在 Foo.prototype 上修复丢失的 constructor 属性 // 新对象属性起到 Foo.prototype 的作用 Object.defineProperty(Foo.prototype, 'constructor', { enumerable: false, writable: true, configurable: true, value: Foo // 让 constructor 指向 Foo }) var a = new Foo() a // Foo { ___proto___: { constructor: ƒ Foo() } }

修复 constructor 需要很多手动操作。所有这些工作都是源于把「constructor」错误地理解为「由……构造」,这个误解的代价实在太高了。

实际上,对象的 constructor 会默认指向一个函数,这个函数可以通过对象的 prototype 引用。constructor」和「prototype」这两个词的含义可能适用也可能不适用。最好的办法是记住这一点:「constructor」并不表示被构造。

a.constructor 并不表示由 Foo 构造,而是委托给 Foo

constructor 并不是一个不可变属性。它是不可枚举的,但是它的值是可写的。此外,你可以给任意 [[Prototype]] 链中的任意对象添加一个名为 constructor 的属性对其进行修改,你可以任意对其赋值。

和 [[Get]] 算法查找 [[Prototype]] 链的机制一样,constructor 属性引用的目标可能和你想的完全不同。

现在你应该明白这个属性多么随意了吧。

似乎这个属性被修改了,对于使用 new 实例新对象也不影响。

结论:一些随意的对象属性引用,比如 a.constructor 实际上是不被信任的,它们不一定会指向默认的函数引用。此外,很快我们就会看到,稍不留神 a.constructor 就可能会指向你意想不到的地方。

a.constructor 是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用。

原型继承

我们已经看到过了许多 JavaScript 程序中常用的模拟类行为的方法,但是如果没有「继承」机制的话,JavaScript 中的类就只是一个空架子。

实际上,我们已经了解了被称作是原型继承的机制,a 可以「继承」 Foo.prototype 并访问 Foo.prototype 的 myName 函数。但是我们之前只把继承看作是类是类之间的关系,并没有把它看作是类和实例之间的关系:

prototype定义自己的方法的作用(Prototype)(2)

还记得这张图吗?它不仅展示出对象 a1 到 Foo.prototype 的委托关系,还展示出 Bar.prototype 到 Foo.prototype 的委托关系,而后者和类继承很相似,只有箭头的方向不同。图中由下到上的箭头表明这是委托关联,不是复制操作。

下面这段代码使用的就是典型的「原型风格」:

function Foo(name) { this.name = name } Foo.prototype.myName = function() { return this.name } function Bar(name,lable) { Foo.call(this,name) this.lable = lable } Bar.prototype = Object.create( Foo.prototype ) // 注意,现在没有 Bar.prototype.constructor 属性了,如果你需要的话,要修复一下,不过大部分,你并不需要它。 Bar.prototype.myLabel = function() { return this.lable } var a = new Bar('foo','bar') a.myName() // 'foo' a.myLabel() // 'bar'

这段代码的核心是语句 Bar.prototype = Object.creat( Foo.prototype )。调用 Object.create(..) 会凭空创建一个新对象,并把新对象内部的 [[Prototype]] 关联到你指定的对象,在这里是 Bar.prototype。

Object.create(..) 创建一个新对象,并把新对象的 [[Prototype]] 指向指定的对象。

换句话说,这条语句的意思是:「创建一个新的 Bar.prototype 对象,并把它关联到 Foo.prototype。」

声明 function Bar() { .. } 时,和其他函数一样,Bar 会有一个 prototype 关联到默认的对象,但是这个对象并不是我们想要的 Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上面,直接把原始的关联对象抛弃掉。

Bar.prototype = Object.create( Foo.prototype ) 就是这句代码。

注意:下面这两种方式是常见的错误做法,实际上它们都存在一些问题:

// 和你想要的不一样! Bar.prototype = Foo.prototype; // 基本上满足你的需求,但是可能会产生一些副作用: Bar.prototype = new Foo()

这里指的是 Bar.prototype = Object.create( Foo.prototype ) 替代品。

Bar.prototype = Foo.prototype 是将 Foo 的原型对象指派给 Bar 的原型对象,这里为什么说想要的不一样呢?

Bar.prototype = Foo.prototype 并不会创建一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接去引用 Foo.prototype 对象本身。显然这不是你想要的结果,否则你根本不需要 Bar 对象,直接使用 Foo 就可以了,这样代码也会简单一点。

一个是引用,一个是创建新对象。

Bar.prototype = new Foo() 的确会创建一个关联到 Bar.prototype 的新对象。但是它使用了 Foo(..) 的「构造函数调用」,如果函数 Foo 有一些副作用,比如写日志、修改状态等的话,就会影响 Bar 的后代,后果不堪设想。

这里并不想和 Foo 函数有关联,只是想和 Foo.prototype 有关联。

因此,要创建一个合适的关联对象,我们必须使用 Object.create(..) 而不是具有副作用的 Foo(..)。这样做唯一的缺点就是需要创建一个新的对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。

Bar.prototype = Object.create( Foo.prototype ) 会直接覆盖掉原有的 Bar.prototype 默认对象。constructor 属性已经被覆盖无了。

如果能有一个标准并且可靠的方法来修改对象的 [[Prototype]] 关联就好了。在 ES6 之前,我们只能通过设置 .proto 属性来实现,但是这个方法并不是标准,并且无法兼容所有的浏览器。ES6 添加了辅助函数 Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。

.proto 是浏览器设置的属性,用来表示对象的原型。

我们来对比一下两种把 Bar.prototype 关联到 Foo.prototype 的方法:

// ES6 之前需要抛弃默认的 Bar.prototype Bar.prototype = Object.create( Foo.prototype ) // ES6 开始可以直接修改现有的 Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype );

如果忽略掉 Object.create(..) 方法带来的轻微性能损失( 抛弃的对象要进行垃圾回收),它实际上比 ES6 及其之后的方法可读性更高并且更短。

检查「类」关系

假设对象 a,如何寻找对象 a 委托的对象呢?在传统的面向类环境中,检查一个实例的继承祖先通常被称为内省(或者反射)。

思考下面的代码:

function Foo() { } Foo.prototype.blah = ... var a = new Foo()

我们如何通过内省找出 a 的「祖先」(委托关联)呢?第一种方法是站在「类」的角度来判断:

a instanceof Foo // true

instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof 回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?

可惜,这个方法只能处理对象和函数之间的关系。如果你想判断两个对象之间是否通过 [[Prototype]] 链关联,只用 instanceof 无法实现。

我不明白,什么叫两个对象是否通过 [[Prototype]] 关联。

指的是 a.prototype = Object.create( Foo.prototype ) 这个语句吗?

问题是命名都用 new 了,我为什么要判断是否通过 [[Prototype]] 关联呢?

脱裤子放屁,多此一举。

下面这段荒谬的代码试图站在「类」的角度使用 instanceof 来判断两个对象的关系:

// 用来判断 o1 是否关联到 o2 的辅助函数 function isRelatedTo(o1, o2) { function F() {} F.prototype = o2 return o1 instanceof F } var a = {c:1} var b = Object.create( a ) b // { ___proto___: { c: 1 } } isRelatedTo(b, a) // true

在 isRelatedTo(..) 内部我们声明了一个一次性函数 F,把它的 prototype 重新赋值并指向对象 o2,然后判断 o1 是否是 F 的一个「实例」。显而易见,o1 实际上并没有继承 F 也不由 F 构造,所以这种方法非常愚蠢并且容易造成误解。问题的关键在于思考的角度,强行在 JavaScript 中应用类的语义就会造成这种尴尬的局面。

说真的,我不明白它这段代码是想要做什么,非常的莫名其妙。

下面是第二种判断 [[Prototype]] 反射的方法,它更加简洁:

Foo.prototype.isPrototypeOf( a ) // true

我不明白,Foo 这个函数在上面代码里有出现过吗?内部函数和 a 有鸡毛关系?

注意:在本例中,我们实际并不关心(甚至不需要 Foo),我们只需要一个可以用来判断的对象就行,isPrototypeOf(..) 回答的问题是:在 a 的整条 [[Prototype]] 链中是否出现过 Foo.prototype?

自信点,把甚至两个字去了,你真的不需要 Foo。

function Foo() {}var b = {}var a = new Foo()Foo.prototype.isPrototypeOf( a ) // trueFoo.prototype.isPrototypeOf( b ) // false

加一段代码不行吗?距离说明更清楚。

同样的问题,同样的答案,但是在第二中方法中并不需要间接引用函数 Foo,它的 prototype 属性会自动被访问。

我不明白他这句话是什么意思。

没有例子的说明,显得如此的混乱。

我们只需要两个对象就可以判断它们之间的关系,举例来说:

// 非常简单,b 是否出现在 c 的 [[Prototype]] 链中? b.isPrototypeOf( c )

注意:这个方法并不需要使用函数,它直接使用 b 和 c 之间的对象来引用来判断它们的关系。换句话说,语言内置的 isPrototypeOf(..) 函数就是我们的 isRealtedTo(..) 函数。

以上所有莫名奇妙的解释和代码就为了解释 isPrototype 方法可以用来判断原型链上是否与另一个对象相关。

这段写的不怎么样。

我们也可以直接获取一个对象的 [[Prototype]] 链。在 ES5 中,标准的方法是:

Object.getPrototypeOf( a )

可以验证一下,这个对象引用是否和我们想的一样:

function Foo() { this.a= 100 } var a = new Foo() Object.getPrototypeOf( a ) === Foo.prototype // true

绝大多数浏览器也支持一种非标准的方法来访问内部 [[Prototype]] 属性:

a.__proto__ === Foo.prototype // true

这个奇怪的 .proto 属性「神奇地」引用了内部的 [[Prototype]] 对象,如果你想查找原型链的话,这个方法非常有用。

都不是标准了,就别用了吧。

和我们之间说过的 .constructor 一样,.proto 实际上并不存在于你正在使用的对象中。实际上,它和其他的常用函数一样,存在于内置的 Object.prototype 中。

我以前一直不明白,为什么原型链似乎是无限互相引用的,现在我明白了,压根就没有复制,而是委托。

JavaScript 的继承机制是委托。!!!

甚至连继承这个词语都不准确。

此外,.proto 看起来很像一个属性,但是实际上更像一个 getter/setter。

.proto 的实现大致上是这样的。

Object.defineProperty( Object.prototype, '__proto__', { get:function() { return Object.getPrototypeOf( this ) }, set: function(o) { Object.setPrototypeOf( this, o ) return o } })

因此,访问 a.proto 时,实际上是调用了 a.proto()。虽然 getter 函数存在于 Object.prototype 对象中,但是它的 this 指向对象 a,所以和 Object.getPrototypeOf( a ) 结果相同。

a.proto 就是调用 调用 get 函数,即 a.proto()

.proto 是可设置属性,之前的代码中使用 ES6 的 Object.setPrototypeOf(..) 进行设置。通常来说,你不需要修改已有对象的 [[Prototype]]。

Object.setPrototypeOf(a, b) 将 b 设置为 a 的原型。

一些框架会使用非常复杂和高端的技术来实现「子类」机制,但是通常来说,我们不推荐这种用法,因为这会极大地增加了代码的阅读难度和维护难度。

原来你知道啊。现在说的这些东西,有几个能用上???除非刻意去用。。

我们只有在一些特殊情况下需要设置默认 .prototype 对象的 [[Prototype]],让它引用其他对象。这样可以避免使用全新的对象替换默认对象。此外,最好把 [[Prototype]] 对象关联看作是只读特性,从而增加代码的可读性。

对象关联

现在我们知道了, [[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他对象

通常来说,这个链接的作用是:如果在对象上内部没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]],以此类推。这一一系列对象的链接称为「原型链」。

对,原型链就是这么个东西。

创建关联

我们已经明白了为什么 JavaScript 的 [[Prototype]] 机制和类不一样,也明白了它如何建立对象间的关联。

那么 [[Prototype]] 机制的意义是什么呢?为什么 JavaScript 开发者费这么大力气在代码中这些关联呢?

就目前为止,并不清楚原型的用处是什么,只知道它是什么,怎么形成的。

还记得吗,本章前面曾经说过 Object.create(..) 是一个大英雄,现在是时候来弄明白为什么了:

var foo = { something: function() { console.log( 'tell me something' ) } } var bar = Object.create( foo ) bar.something() // 'tell me something'

Object.create(..) 会创建一个新对象并把它关联到我们指定的对象,这样我们就可以充分发挥 [[Prototype]] 机制的威力并且避免不必要的麻烦,比如使用 new 的构造函数会生成 .prototype 和 .constructor 引用。

这里的 new 应该指的是 new bar()

我们并不需要来创建两个对象之间的关系,只需要通过委托关联对象就足够了。而 Object.create(..) 不包含任何「类的诡计」,所以它可以完美地创建我们想要的关联关系。

关联关系是否备用

看起来对象之间的关联关系是处理「缺失」属性或者方法的一种备用选项。这个说法有点道理,但是我认为这不是 [[Prototype]] 的本质。

var anotherObject = { cool: function() { console.log( 'cool' ) } } var myObject = Object.create( anotherObject ) myObject.cool() // 'cool'

由于存在 [[Prototype]] 机制,这段代码可以正常工作。但是如果你这样写只是为了让 myObject 在无法处理属性或者方法时使用被用的 anotherObject,那么你的软件就会变得有点「神奇」,而且很难理解和维护。

这并不是说任何情况都不应该选择备用这种设计模式,但是这在 JavaScript 中并不是不是很常见。所以如果你使用的是这种模式,那或许应该退后一步并重新思考一下这种模式是否适用。

当你给开发设计软件时,假设要调用 myObject.cool(),myObject 中不存于 cool() 时这条语句也正常工作的话,那你的 API 设计就会变得很「神奇」,对于未来维护你软件的开发者来说这可能不太好理解。

但是你可以让你的 API 设计不那么「神奇」,同时仍能发挥 [[Prototype]] 关联的威力:

var anotherObject = { cool: function() { console.log( 'cool' ) } } var myObject = Object.create( anotherObject ) myObject.doCool = function() { this.cool() // 内部委托 } myObject.doCool() // cool

这里我们调用的 myObject.doCool() 实际上存在于 myObject 中,这可以让我们 Api 设计更加清晰。从内部来讲,我们的实现遵循的是委托设计模式,通过 [[Prototype]] 委托到 anotherObject.cool()

换句话说,内部委托比起直接委托可以让 API 接口设计更加清晰。

好像确实看起来更好一点

小结

如果要访问对象中不存在的一个属性, [[Get]] 操作就会查找对象内部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条「原型链」,在查找属性时会对它进行遍历。

层层往上。

所有普通对象都有内置的 Object.prototype,指向原型链的顶端,如果在原型链中找不到找不到指定的属性就会停止。 toString()、valueOf() 和其他的一些通用的功能都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。

最外层是 Object.prototype

关联两个对象最常用的方法是使用 new 关键字进行函数调用,在调用的 4 个步骤中会创建一个关联其他对象的新对象。

使用 new 调用函数时会把新对象的 .prototype 属性关联到「其他对象」。带 new 的函数调用通常被称为「构造函数调用」,尽管它们实际上和传统面向类语言中的类构造函数不一样。

虽然这些 JavaScript 机制和传统面向类语言中的「类初始化」和「类继承」很相似,但是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。

出于各种理由,以「继承」结尾的术语和其他面向对象的术语都无法帮助理解 JavaScript 真实机制。

相比之下,「委托」是一个更合适的术语,因为对象之间的关系不是复制而是委托。

JavaScript 这门语言充满看似,实际上并没有继承,所有的继承都是委托假装出来的样子。

,