77
88在做代码复用的工作的时候,谨记Gang of Four 在书中给出的关于对象创建的建议:“优先使用对象创建而不是类继承”。(译注:《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)是一本设计模式的经典书籍,该书作者为Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides,被称为“Gang of Four”,简称“GoF”。)
99
10-
10+ < a name = " a2 " ></ a >
1111## 类式继承 vs 现代继承模式
1212
1313在讨论JavaScript的继承这个话题的时候,经常会听到“类式继承”的概念,那我们先看一下什么是类式(classical)继承。classical一词并不是来自某些古老的、固定的或者是被广泛接受的解决方案,而仅仅是来自单词“class”。(译注:classical也有“经典”的意思。)
3030
3131本章先讨论类式继承,然后再关注现代继承模式。
3232
33+ <a name =" a3 " ></a >
3334## 类式继承的期望结果
3435
3536实现类式继承的目标是基于构造函数Child()来创建一个对象,然后从另一个构造函数Parent()获得属性。
5657
5758上面的代码定义了两个构造函数Parent()和Child(),say()方法被添加到了Parent()构建函数的原型(prototype)中,inherit()函数完成了继承的工作。inherit()函数并不是原生提供的,需要自己实现。让我们来看一看比较大众的实现它的几种方法。
5859
60+ <a name =" a4 " ></a >
5961## 类式继承1——默认模式
6062
6163最常用的一种模式是使用Parent()构造函数来创建一个对象,然后把这个对象设为Child()的原型。这是可复用的inherit()函数的第一种实现方法:
7173 var kid = new Child();
7274 kid.say(); // "Adam"
7375
76+ <a name =" a5 " ></a >
7477### 跟踪原型链
7578
7679在这种模式中,子对象既继承了(父对象)“自己的属性”(添加给this的实例属性,比如name),也继承了原型中的属性和方法(比如say())。
@@ -107,6 +110,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
107110
108111如果通过delete kid.name的方式移除新添加的属性,那么2号对象的name属性将暴露出来并且在查找的时候被找到。
109112
113+ <a name =" a6 " ></a >
110114### 这种模式的缺点
111115
112116这种模式的一个缺点是既继承了(父对象)“自己的属性”,也继承了原型中的属性。大部分情况下你可能并不需要“自己的属性”,因为它们更可能是为实例对象添加的,并不用于复用。
@@ -120,7 +124,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
120124
121125这并不是我们期望的结果。事实上传递参数给父构造函数是可能的,但这样需要在每次需要一个子对象时再做一次继承,很不方便,因为需要不断地创建父对象。
122126
123-
127+ < a name = " a7 " ></ a >
124128## 类式继承2——借用构造函数
125129
126130下面这种模式解决了从子对象传递参数到父对象的问题。它借用了父对象的构造函数,将子对象绑定到this,同时传入参数:
@@ -165,7 +169,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
165169
166170在这个例子中,blog对象修改了tags属性,同时,它也修改了父对象,因为实际上blog.tags和article.tags是引向同一个数组。而对pages.tags的修改并不影响父对象article,因为pages.tags在继承的时候是一份独立的拷贝。
167171
168-
172+ < a name = " a8 " ></ a >
169173### 原型链
170174
171175我们来看一下当我们使用熟悉的Parent()和Child()构造函数和这种继承模式时原型链是什么样的。为了使用这种继承模式,Child()有明显变化:
@@ -195,7 +199,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
195199
196200图6-4 使用借用构造函数模式时没有被关联的原型链
197201
198-
202+ < a name = " a9 " ></ a >
199203### 利用借用构造函数模式实现多继承
200204
201205使用借用构造函数模式,可以通过借用多个构造函数的方式来实现多继承:
@@ -226,7 +230,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
226230
227231图6-5 在Firebug中查看CatWings对象
228232
229-
233+ < a name = " a10 " ></ a >
230234### 借用构造函数的利与弊
231235
232236这种模式的一个明显的弊端就是无法继承原型。如前面所说,原型往往是添加可复用的方法和属性的地方,这样就不用在每个实例中再创建一遍。
@@ -235,7 +239,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
235239
236240那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能使kid可以访问到say()方法呢?下一种继承模式解决了这个问题。
237241
238-
242+ < a name = " a11 " ></ a >
239243## 类式继承3——借用并设置原型
240244
241245综合以上两种模式,首先借用父对象的构造函数,然后将子对象的原型设置为父对象的一个新实例:
@@ -281,7 +285,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
281285
282286图6-6 除了继承“自己的属性”外,原型链也被保留了
283287
284-
288+ < a name = " a12 " ></ a >
285289## 类式继承4——共享原型
286290
287291不像前一种类式继承模式需要调用两次父构造函数,下面这种模式根本不会涉及到调用父构造函数的问题。
@@ -300,7 +304,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
300304
301305图6-7 (父子对象)共享原型时的关系
302306
303-
307+ < a name = " a13 " ></ a >
304308## 类式继承5——临时构造函数
305309
306310下一种模式通过打断父对象和子对象原型的直接链接解决了共享原型时的问题,同时还从原型链中获得其它的好处。
@@ -327,7 +331,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
327331
328332如果你访问kid.name将得到undefined。在这个例子中,name是父对象自己的属性,而在继承的过程中我们并没有调用new Parent(),所以这个属性并没有被创建。当访问kid.say()时,它在3号对象中不可用,所以在原型链中查找,4号对象也没有,但是1号对象有,它在内在中的位置会被所有从Parent()创建的构造函数和子对象所共享。
329333
330-
334+ < a name = " a14 " ></ a >
331335### 存储父类(Superclass)
332336
333337在上一种模式的基础上,还可以添加一个指向原始父对象的引用。这很像其它语言中访问超类(superclass)的情况,有时候很方便。
@@ -341,7 +345,7 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这
341345 C.uber = P.prototype;
342346 }
343347
344-
348+ < a name = " a15 " ></ a >
345349### 重置构造函数引用
346350
347351这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数(constructor)的指向,以便未来在某个时刻能被正确地使用。
@@ -386,7 +390,7 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以
386390 }
387391 }());
388392
389-
393+ < a name = " a16 " ></ a >
390394## Klass
391395
392396有很多JavaScript类库模拟了类,创造了新的语法糖。具体的实现方式可能会不一样,但是基本上都有一些共性,包括:
@@ -486,7 +490,7 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以
486490
487491什么时候使用这种模式?其实,最好是能避免则避免,因为它带来了在这门语言中不存在的完整的类的概念,会让人疑惑。使用它需要学习新的语法和新的规则。也就是说,如果你或者你的团队对类感到习惯并且同时对原型感到不习惯,这种模式可能是一个可以探索的方向。这种模式允许你完全忘掉原型,好处就是你可以将语法变种得像其它你所喜欢的语言一样。
488492
489-
493+ < a name = " a17 " ></ a >
490494## 原型继承
491495
492496现在,让我们从一个叫作“原型继承”的模式来讨论没有类的现代继承模式。在这种模式中,没有任何类进来,在这里,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法:
@@ -514,8 +518,11 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以
514518
515519图6-9展示了使用原型继承时的原型链。在这里child总是以一个空对象开始,它没有自己的属性但通过原型链(\_\_ proto\_\_ )拥有父对象的所有功能。
516520
517- //TODO:图6-9
521+ ![ 图6-9 原型继承模式 ] ( ./Figure/chapter6/6-9.jpg )
518522
523+ 图6-9 原型继承模式
524+
525+ <a name =" a18 " ></a >
519526### 讨论
520527
521528在原型继承模式中,parent不需要使用对象字面量来创建。(尽管这是一种更觉的方式。)可以使用构造函数来创建parent。注意,如果你这样做,那么自己的属性和原型上的属性都将被继承:
@@ -556,7 +563,8 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以
556563
557564 typeof kid.getName; // "function", because it was in the prototype
558565 typeof kid.name; // "undefined", because only the prototype was inherited
559-
566+
567+ <a name =" a19 " ></a >
560568###例外的ECMAScript 5
561569
562570在ECMAScript 5中,原型继承已经正式成为语言的一部分。这种模式使用Object.create方法来实现。换句话说,你不再需要自己去写类似object()的函数,它是语言原生的了:
@@ -576,7 +584,7 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
576584 var child = Y.Object(parent);
577585 });
578586
579-
587+ < a name = " a20 " ></ a >
580588## 通过复制属性继承
581589
582590让我们来看一下另外一种继承模式——通过复制属性继承。在这种模式中,一个对象通过简单地复制另一个对象来获得功能。下面是一个简单的实现这种功能的extend()函数:
@@ -652,9 +660,10 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
652660
653661这种模式并不高深,因为根本没有原型牵涉进来,而只跟对象和它们的属性有关。
654662
663+ <a name =" a21 " ></a >
655664## 混元(Mix-ins)
656665
657- 既然谈到了通过拷贝属性来继随 ,就让我们顺便多说一点,来讨论一下“混元”模式。除了前面说的从一个对象复制,你还可以从任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象。
666+ 既然谈到了通过复制属性来继承 ,就让我们顺便多说一点,来讨论一下“混元”模式。除了前面说的从一个对象复制,你还可以从任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象。
658667
659668实现很简单,只需要遍历传入的每个参数然后复制它们的每个属性:
660669
@@ -687,6 +696,7 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
687696
688697> 如果你习惯了某些将混元作为原生部分的语言,那么你可能期望修改一个或多个父对象时也影响子对象。但在这个实现中这是不会发生的事情。这里我们只是简单地遍历、复制自己的属性,并没有与父对象的链接。
689698
699+ <a name =" a22 " ></a >
690700## 借用方法
691701
692702有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。受益于函数方法call()和apply(),通过借用方法模式,这是可行的。在本书中,你其实已经见过这种模式了,甚至在本章extendDeep()的实现中也有用到。
@@ -702,6 +712,7 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
702712
703713你传一个对象和任意的参数,这个被借用的方法会将this绑定到你自己的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(指你不需要的属性和方法)。
704714
715+ <a name =" a23 " ></a >
705716### 例:从数组借用
706717
707718这种模式的一种常见用法是从数组借用方法。
@@ -718,6 +729,7 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
718729
719730在这个例子中,有一个空数组被创建了,因为要借用它的方法。同样的事情也可以使用一种看起来代码更长的方法来做,那就是直接从数组的原型中借用方法,使用Array.prototype.slice.call(...)。这种方法代码更长一些,但是不用创建一个空数组。
720731
732+ <a name =" a24 " ></a >
721733### 借用并绑定
722734
723735当借用方法的时候,不管是通过call()/apply()还是通过简单的赋值,方法中的this指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将this的值锁定或者提前绑定到一个指定的对象上。
@@ -775,6 +787,7 @@ Object.create()接收一个额外的参数——一个对象。这个额外对
775787
776788绑定是奢侈的,你需要付出的代价是一个额外的闭包。
777789
790+ <a name =" a25 " ></a >
778791### Function.prototype.bind()
779792
780793ECMAScript5在Function.prototype中添加了一个方法叫bind(),使用时和apply和call()一样简单。所以你可以这样写:
@@ -807,11 +820,12 @@ ECMAScript5在Function.prototype中添加了一个方法叫bind(),使用时和
807820 var twosay3 = one.say.bind(two, 'Enchanté');
808821 twosay3(); // "Enchanté, another object"
809822
823+ <a name =" a26 " ></a >
810824##小结
811825
812826在JavaScript中,继承有很多种方案可以选择。学习和理解不同的模式是有好处的,因为这可以增强你对这门语言的掌握能力。在本章中你看到了很多类式继承和现代继承的方案。
813827
814- 但是,也许在开发过程中继承并不是你经常面对的一个问题。这一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在JavaScript中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以利用代码的方法,但在JavaScript中你可能有更多更简单更优化的方法,包括借用方法、绑定、拷贝属性 、混元等。
828+ 但是,也许在开发过程中继承并不是你经常面对的一个问题。这一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在JavaScript中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以利用代码的方法,但在JavaScript中你可能有更多更简单更优化的方法,包括借用方法、绑定、复制属性 、混元等。
815829
816830记住,代码复用才是目标,继承只是达成这个目标的一种手段。
817831
0 commit comments