企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 2.5 原型 ### 2.5.1 [[Prototype]] JavaScript 中的对象有一个特殊的`[[Prototype]]` 内置属性,其实就是**对于其他对象的引用**。几乎所有的对象在创建时[[Prototype]] 属性都会被赋予一个非空的值。 ~~~ var myObject = { a:2 }; myObject.a; // 2 ~~~ [[Prototype]] 引用有什么用呢? 当你试图引用对象的属性时会触发`[[Get]]` 操作,比如`myObject.a`。对于默认的`[[Get]]` 操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。 但是如果a 不在myObject 中,就需要使用对象的[[Prototype]] 链了。对于默认的[[Get]] 操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]] 链: ~~~ var anotherObject = { a:2 }; // 创建一个关联到anotherObject 的对象 var myObject = Object.create( anotherObject ); myObject.a; // 2 ~~~ 现在myObject 对象的[[Prototype]] 关联到了anotherObject。显然myObject.a 并不存在,但是尽管如此,属性访问仍然成功地(在anotherObject 中)找到了值2。 但是,如果anotherObject 中也找不到a 并且[[Prototype]] 链不为空的话,就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]] 链。如果是后者的话,[[Get]] 操作的返回值是`undefined`。 使用`for..in` 遍历对象时原理和查找[[Prototype]] 链类似,任何可以通过原型链访问到(并且是enumerable)的属性都会被枚举。使用`in `操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举): ~~~ var anotherObject = { a:2 }; // 创建一个关联到anotherObject 的对象 var myObject = Object.create( anotherObject ); for (var k in myObject) { console.log("found: " + k); } // found: a ("a" in myObject); // true ~~~ 因此,当你通过各种语法进行属性查找时都会查找[[Prototype]] 链,直到**找到属性或者查找完整条原型链**。 **1. Object.prototype** 所有普通的`[[Prototype]] `链最终都会指向内置的`Object.prototype`。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]] 链的顶端设置为)这个`Object.prototype` 对象,所以它包含JavaScript 中许多通用的功能。比如说`.toString()` , `.valueOf()` , ` .hasOwnProperty(..)` 和下面介绍的` .isPrototypeOf(..)`。 **2. 属性设置和屏蔽** `2.3`说过,给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。 ~~~ myObject.foo = "bar"; ~~~ * 如果myObject 对象中包含名为foo 的普通数据访问属性,这条赋值语句只会修改已有的属性值。 * 如果foo 不是直接存在于myObject 中,[[Prototype]] 链就会被遍历,类似[[Get]] 操作。如果原型链上找不到foo,foo 就会被直接添加到myObject 上。(如果foo 存在于原型链上层,赋值语句my`Object.foo = "bar" `的行为就会有些不同) * 如果属性名foo 既出现在myObject 中也出现在myObject 的[[Prototype]] 链上层, 那么就会发生**屏蔽**。myObject 中包含的foo 属性会屏蔽原型链上层的所有foo 属性,因为myObject.foo 总是会选择原型链中最底层的foo 属性。 下面分析一下如果foo 不直接存在于myObject 中而是存在于原型链上层时myObject.foo = "bar" 会出现的三种情况。 * 1)如果在`[[Prototype]] `链上层存在名为`foo `的普通数据访问属性并且没有被标记为只读(`writable:false`),那就会**直接**在`myObject` 中添加一个名为`foo` 的新属性,它是**屏蔽**属性。 * 2)如果在`[[Prototype]]` 链上层存在`foo`,但是它被标记为只读(`writable:false`),那么无法修改已有属性或者在myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,**不会发生屏蔽**。 * 3)如果在`[[Prototype]]` 链上层存在`foo` 并且它是一个`setter`,那就一定会调用这个`setter`。`foo `不会被添加到(或者说屏蔽于)`myObject`,也不会重新定义`foo` 这个`setter`。 ~~~ 如果你希望在第二种和第三种情况下也屏蔽foo,那就不能使用= 操作符来赋值, 而是使用Object.defineProperty(..)来向myObject 添加foo。 如果需要对屏蔽方法进行委托的话就不得不使用丑陋的显式伪多态。通常来说,使用屏蔽得不偿失,所以应当尽量避免使用。 ~~~ 有些情况下会**隐式产生屏蔽**,一定要当心。思考下面的代码: ~~~ var anotherObject = { a:2 }; var myObject = Object.create( anotherObject ); anotherObject.a; // 2 myObject.a; // 2 anotherObject.hasOwnProperty( "a" ); // true myObject.hasOwnProperty( "a" ); // false myObject.a++; // 隐式屏蔽! anotherObject.a; // 2 myObject.a; // 3 myObject.hasOwnProperty( "a" ); // true ~~~ 尽管`myObject.a++` 看起来应该(通过委托)查找并增加`anotherObject.a `属性,但是别忘了++ 操作相当于`myObject.a = myObject.a + 1`。因此++ 操作首先会通过[[Prototype]]查找属性a 并从anotherObject.a 获取当前属性值2,然后给这个值加1,接着用**`[[Put]]`**将值3 赋给myObject 中新建的屏蔽属性a。 修改委托属性时一定要小心。如果想让anotherObject.a 的值增加,**唯一**的办法是`anotherObject.a++`。 ### 2.5.2 “类” 在JavaScript 中,类无法描述对象的行,(因为根本就不存在类!)对象直接定义自己的行为。**JavaScript 中只有对象。** **1. “类”函数** JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是**模仿类**。“类似类”的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为`prototype` 的公有并且不可枚举的属性,它会指向另一个对象: ~~~ function Foo() { // ... } Foo.prototype; // { } ~~~ 这个对象通常被称为`Foo` 的**原型**,因为我们通过名为`Foo.prototype `的属性引用来访问它。 `Foo` 的原型这个对象是在调用new Foo()时创建的,最后会被(有点武断地)关联到这个“Foo 点prototype”对象上。 ~~~ function Foo() { // ... } var a = new Foo(); Object.getPrototypeOf( a ) === Foo.prototype; // true ~~~ 调用new Foo() 时会创建a,其中的一步就是给a 一个内部的`[[Prototype]]` 链接,关联到Foo.prototype 指向的那个对象。 在面向类的语言中,类可以被复制(或者说实例化)多次,是因为实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。但是在JavaScript 中,并**没有类似的复制机制**。你不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]] 关联的是同一个对象。但是在默认情况下并**不会进行复制**,因此这些对象之间并不会完全失去联系,它们是**互相关联**的。 new Foo() 会生成一个新对象(我们称之为a),这个新对象的内部链接[[Prototype]] 关联的是Foo.prototype 对象。最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。 new Foo() 这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo() 只是间接完成了我们的目 标:**一个关联到其他对象的新对象**。 #### 关于名称 在JavaScript 中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。 ![](https://box.kancloud.cn/70ecd7cd5c76556d87e76e85f828c3cd_696x333.png) 这个机制通常被称为**原型继承**,它常常被视为动态语言版本的类继承。这个名称主要是为了对应面向类的世界中“继承”的意义,但是违背(推翻)了动态脚本中对应的语义。 **继承**意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过**委托**访问另一个对象的属性和函数。**委托**这个术语可以更加准确地描述JavaScript 中对象的关联机制。 **2. “构造函数”** ~~~ function Foo() { // ... } var a = new Foo(); ~~~ 到底是什么让我们认为Foo 是一个“类”呢? * 关键字`new`,在面向类的语言中构造类实例时也会用到它。看起来我们执行了类的构造函数方法,Foo() 的调用方式很像初始化类时类构造函数的调用方式。 * 属性`.constructor` ~~~ function Foo() { // ... } Foo.prototype.constructor === Foo; // true var a = new Foo(); a.constructor === Foo; // true ~~~ Foo.prototype 默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性`.constructor`,这个属性引用的是对象关联的函数(本例中是Foo)。此外,我们可以看到通过“构造函数”调用`new Foo() `创建的对象也有一个`.constructor `属性,指向“创建这个对象的函数”。 ~~~ 实际上a 本身并没有.constructor 属性。而且,虽然a.constructor 确实指向Foo 函数,但是这个属性并不是表示a 由Foo“构造”。 ~~~ #### 构造函数还是调用 实际上,Foo 和程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当在普通的函数调用前面加上new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。 举例来说: ~~~ function NothingSpecial() { console.log( "Don't mind me!" ); } var a = new NothingSpecial(); // "Don't mind me!" a; // {} ~~~ `NothingSpecial `只是一个普通的函数,但是使用new 调用时,它就会构造一个对象并赋值给a,这看起来像是new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数调用,但是NothingSpecial 本身并不是一个构造函数。换句话说,在JavaScript 中对于“构造函数”最准确的解释是,所有带new 的函数调用。函数不是构造函数,但是当且仅当使用new 时,函数调用会变成“**构造函数调用**”。 **3. 技术** ~~~ 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 `给每个对象(也就是a 和b)都添加了.name 属性,有点像类实例封装的数据值。 * 2)`Foo.prototype.myName = ... `,它会给Foo.prototype 对象添加一个属性(函数)。 在这段代码中,看起来似乎创建a 和b 时会把Foo.prototype 对象复制到这两个对象中,然而事实并不是这样。在创建的过程中,a 和b 的内部[[Prototype]] 都会关联到Foo.prototype 上。当a和b 中无法找到myName 时,它会(通过委托)在Foo.prototype 上找到。 #### 回顾“构造函数” 之前讨论`.constructor` 属性时,看起来`a.constructor === Foo `为真意味着a 确实有一个指向`Foo 的.constructor` 属性,但是事实不是这样。实际上,`.constructor` 引用同样被**委托**给了`Foo.prototype`,而`Foo.prototype.constructor` 默认指向Foo。 举例来说,`Foo.prototype` 的`.constructor` 属性只是Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的.prototype 对象引用,那么新对象并不会自动获得.constructor 属性。 思考下面的代码: ~~~ function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 创建一个新原型对象 var a1 = new Foo(); a1.constructor === Foo; // false! a1.constructor === Object; // true! ~~~ a1 并没有`.constructor` 属性,所以它会委托[[Prototype]] 链上的`Foo.prototype`。但是这个对象也没有.constructor 属性(不过默认的Foo.prototype 对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的`Object.prototype`。这个对象有`.constructor `属性,指向内置的`Object(..) `函数。 当然,你可以给`Foo.prototype` 添加一个`.constructor` 属性,不过这需要手动添加一个符合正常行为的不可枚举属性。 举例来说: ~~~ function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 创建一个新原型对象 // 需要在Foo.prototype 上“修复”丢失的.constructor 属性 // 新对象属性起到Foo.prototype 的作用 // 关于defineProperty(..),参见第3 章 Object.defineProperty( Foo.prototype, "constructor" , { enumerable: false, writable: true, configurable: true, value: Foo // 让.constructor 指向Foo } ); ~~~ **修复.constructor 需要很多手动操作。所有这些工作都是源于把“constructor”错误地理解为“由……构造”!** ### 2.5.3(原型)继承 ~~~ 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 Bar.prototype = Object.create( Foo.prototype ); // 注意!现在没有Bar.prototype.constructor 了 // 如果你需要这个属性的话可能需要手动修复一下它 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)。即**创建一个新的Bar.prototype 对象并把它关联到Foo.prototype”**。 声明function Bar() { .. } 时,和其他函数一样,Bar 会有一个.prototype 关联到默认的对象,但是这个对象并不是我们想要的Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。 注意,下面这两种方式是常见的错误做法,实际上它们都存在一些问题: ~~~ // 和你想要的机制不一样! Bar.prototype = Foo.prototype; // 基本上满足你的需求,但是可能会产生一些副作用 :( Bar.prototype = new Foo(); ~~~ * Bar.prototype = Foo.prototype 并不会创建一个关联到Bar.prototype 的新对象,它只是让Bar.prototype 直接引用Foo.prototype 对象。因此当你执行类似`Bar.prototype.myLabel = ...` 的赋值语句时会直接修改`Foo.prototype` 对象本身。显然这不是你想要的结果,否则你根本不需要Bar 对象,直接使用Foo 就可以了,这样代码也会更简单一些。 * `Bar.prototype = new Foo()` 的确会创建一个关联到`Bar.prototype` 的新对象。但是它使用了`Foo(..) `的“构造函数调用”,如果函数Foo 有一些副作用(比如写日志、修改状态、注册到其他对象、给this 添加数据属性,等等)的话,就会影响到Bar() 的“后代”,后果不堪设想。 ES6 添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。 我们来对比一下两种把Bar.prototype 关联到Foo.prototype 的方法: ~~~ // ES6 之前需要抛弃默认的Bar.prototype Bar.ptototype = Object.create( Foo.prototype ); // ES6 开始可以直接修改现有的Bar.prototype Object.setPrototypeOf( Bar.prototype, Foo.prototype ); ~~~ 如果忽略掉Object.create(..) 方法带来的轻微性能损失(抛弃的对象需要进行垃圾回收),它实际上比ES6 及其之后的方法更短而且可读性更高。不过无论如何,这是两种完全不同的语法。 #### 检查“类”关系 假设有对象a,如何寻找对象a 委托的对象(如果存在的话)呢?在传统的面向类环境中,检查一个实例(JavaScript 中的对象)的继承祖先(JavaScript 中的委托关联)通常被称为**内省(或者反射)**。 ~~~ function Foo() { // ... } Foo.prototype.blah = ...; var a = new Foo(); ~~~ 如何通过内省找出a 的“祖先”(委托关联)呢? * 第一种方法,站在“类”的角度来判断: ~~~ a instanceof Foo; // true ~~~ `instanceof `操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof 回答的问题是:在a 的整条[[Prototype]] 链中是否有指向Foo.prototype 的对象? 可惜,这个方法只能处理对象(a)和函数(带.prototype 引用的Foo)之间的关系。如果你想判断两个对象(比如a 和b)之间是否通过[[Prototype]] 链关联,只用instanceof无法实现。 * 第二种方法 ~~~ Foo.prototype.isPrototypeOf( a ); // true ~~~ 在本例中,我们实际上并不关心(甚至不需要)Foo,我们只需要一个可以用来判断的对象(本例中是Foo.prototype)就行。isPrototypeOf(..) 回答的问题是:在a 的整条[[Prototype]] 链中是否出现过Foo.prototype ? 第二种方法中并不需要间接引用函数(Foo),它的`.prototype` 属性会被自动访问。我们只需要两个对象就可以判断它们之间的关系。举例来说: ~~~ // 非常简单:b 是否出现在c 的[[Prototype]] 链中? b.isPrototypeOf( c ); ~~~ 注意,这个方法并不需要使用函数(“类”),它直接使用b 和c 之间的对象引用来判断它们的关系。 我们也可以直接获取一个对象的[[Prototype]] 链。在ES5 中,标准的方法是: ~~~ Object.getPrototypeOf( a ); ------------------------------------------------------- Object.getPrototypeOf( a ) === Foo.prototype; // true ~~~ 绝大多数浏览器也支持一种非标准的方法来访问内部[[Prototype]] 属性: ~~~ a.__proto__ === Foo.prototype; // true ~~~ `.__proto__`( 在ES6 之前并不是标准) 属性引用了内部的`[[Prototype]]` 对象,如果你想直接查找(甚至可以通过`.__proto__.__ptoto__... `来遍历)原型链的话,这个方法非常有用。和之前说过的`.constructor` 一样,`.__proto__` 实际上并不存在于你正在使用的对象中(本例中是a)。实际上,它和其他的常用函数(.toString()、.isPrototypeOf(..),等等)一样,存在于内置的Object.prototype 中。(而且是不可枚举的) `.__proto__ `看起来很像一个属性,但是实际上它更像一个`getter/setter`: ~~~ Object.defineProperty( Object.prototype, "__proto__", { get: function() { return Object.getPrototypeOf( this ); }, set: function(o) { // ES6 中的setPrototypeOf(..) Object.setPrototypeOf( this, o ); return o; } } ); ~~~ 因此,访问(获取值)`a.__proto__ `时,实际上是调用了`a.__proto__()`(调用getter 函数)。虽然`getter` 函数存在于`Object.prototype` 对象中,但是它的this 指向对象a,所以和`Object.getPrototypeOf( a )` 结果相同。 ### 2.5.4 对象关联 `[[Prototype]]` 机制就是存在于对象中的一个内部链接,它会引用其他对象。 通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为**“原型链”**。 **1. 创建关联** ~~~ var foo = { something: function() { console.log( "Tell me something good..." ); } }; var bar = Object.create( foo ); bar.something(); // Tell me something good... ~~~ `Object.create(..) `会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥`[[Prototype]]` 机制的威力(委托)并且避免不必要的麻烦(比如使用new 的构造函数调用会生成.prototype 和.constructor 引用)。 **Object.create()的polyfill代码** Object.create(..) 是在ES5 中新增的函数,所以在ES5 之前的环境中(比如旧IE)如果要支持这个功能的话就需要使用一段简单的polyfill 代码, 它部分实现了Object.create(..) 的功能: ~~~ if (!Object.create) { Object.create = function(o) { function F(){} F.prototype = o; return new F(); }; } ~~~ 这段polyfill 代码使用了一个一次性函数F,我们通过改写它的.prototype 属性使其指向想要关联的对象,然后再使用new F() 来构造一个新对象进行关联。 标准ES5 中内置的Object.create(..) 函数还提供了一系列附加功能: ~~~ var anotherObject = { a:2 }; var myObject = Object.create( anotherObject, { b: { enumerable: false, writable: true, configurable: false, value: 3 }, c: { enumerable: true, writable: false, configurable: false, value: 4 } }); myObject.hasOwnProperty( "a" ); // false myObject.hasOwnProperty( "b" ); // true myObject.hasOwnProperty( "c" ); // true myObject.a; // 2 myObject.b; // 3 myObject.c; // 4 ~~~ Object.create(..) 的第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符。因为ES5 之前的版本无法模拟属性操作符,所以polyfill 代码无法实现这个附加功能。 **2. 关联关系是备用** 看起来对象之间的关联关系是处理“缺失”属性或者方法时的一种备用选项。但这种说法不是`[[Prototype]] `的本质。 ~~~ var anotherObject = { cool: function() { console.log( "cool!" ); } }; var myObject = Object.create( anotherObject ); myObject.cool(); // "cool!" ~~~ 由于存在[[Prototype]] 机制,这段代码可以正常工作。但是如果你这样写只是为了让myObject 在无法处理属性或者方法时可以使用备用的anotherObject,那么你的软件就会变得有点“神奇”,而且很难理解和维护。 ~~~ 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 接口设计更加清晰。