javascript设计模式学习笔记四
4.继承
在JavaScript中继承是一个非常复杂的话题,比其他任何面向对象的语言中的继承都复杂得多。在大多数其他面向对象语言中,继承一个类只需要使用一个关键字即可。与它们不同,在JavaScript中想要表达到传承公用的目的,需要采取一系列措施。更有甚者,JavaScript属于原型式继承(我们会想你证明这其实是一个极大的优点)的少数语言之一。得益于这种语言的灵活性,你既要可使用标准的基于类的继承,也可使用更微妙一些(但也可能更有效一些)的原型式继承。
本章将讨论在JavaScript中创建子类的各种技术以及他们的适用场合。
4.1 为什么需要继承
在开始实际摆弄代码之前,我们先得搞清使用继承能带来什么。一情来说,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合。使用继承符合前一个设计原则的需要。借助这种机制,你可以在现有类的基础上进行设计并充分利用他们已经具备的各种方法,而对设计进行修改也更为轻松。假设你需要让几个类都拥有一个按特定方式输出类结构的tostring方法,当然可以用复制粘贴的方法把定义的tostring方法的代码添加到每一个类中,但这样的话,每当需要改变这个方法的工作方式时,你将不得不在每一个类中重复同样的修改。反之,如果你先创建一个TostringProvider类,然后让那些类继承这个类,那么tostring这个方法只需要一个地方声明即可。
让一个类继承另一个类可能会导致二者产生强耦合,也即一个类依赖另一个类的内部实现。我们将讨论一些有助于避免这种问题的技术,其中包括用掺元类为其他类提供方法这种技术。
4.2 类式继承
JavaScript可以被装扮成使用类似继承的语言。通过用函数来声明类、用关键字new来创建实例,JavaScript中的对象也能惟妙惟肖地模仿Java或c++中的对象。下面是JavaScript中的一个简单的类声明:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Class Person */ 2 function Person(name) { 3 this.name = name; 4 } 5 6 Person.Prototype.getName = function() { 7 return this.name; 8 }
首先要做的是创建构造函数。按惯例,其名称就是类名,首字母应大写。在构造函数中,创建实例属性需要使用关键字this。类的方法则被添加到其prototype对象中,就像例中的Person.prototype.getName那样。要创建该类的实例,只需结合关键字new调用这个构造函数即可:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var reader = new Person('John smith'); 2 reader.getName();
然后你可以访问所有的实例属性,也可以调用所有的实例方法。这是JavaScript中一个非常简单的类的例子。
4.2.1 原型链
创建继承Person的类则要复杂一些:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Class Author */ 2 3 function Author(name,books) { 4 Person.call(this, name); // Call the superClass`constructor in the scope of this. 5 this.books = books; // Add an attribute to Author 6 } 7 8 Author.prototype = new Person(); // set uo the prototye chain. 9 Author.prototype.constructor = Author; // set the constructor attribute to Author. 10 Author.prototype.getBooks = function() { // Add a method to Author 11 return this.books 12 }
让一个类继承另一个类需要用到许多行代码(不像大多数别的面向对象的语言中那样只用一个关键字extend即可)。首先要做的是像前一个示例中那样创建一个构造函数。在构造函数中调用超类的构造函数,并将name参数传给它。这行代码需要解释一下。在使用new运算符的时候,系统会为你做一些事。它先创建一个空对象,然后调用构造函数,在此过程中,这个空对象处于作用域链的最前端。而在Author函数中调用超类的构造函数时,你必须手工完成同样的任务。“Person.call(this, name)”这条语句调用了Person构造函数,并且在此过程中让那个空对象(用this代表)处于作用域链的最前端,而name则被作为参数传入。
下一步是设置原型链。尽管相关代码比较简单,但这实际是一个非常复杂的问题。前面已经说过,JavaScript没有extend关键字。但是在JavaScript中每个对象都有一个名为prototype的属性,这个属性要么指向另一个对象,要么是null。在访问对象的某个成员时(比如reader.getName),如果这个成员未见于当前对象,那么JavaScript会在prototype属性所指的对象中查找它。如果在那个对象中也没有找到,那么JavaScript会沿着原型链向上逐一访问每个原型对象,直到找到这个成员(或已经查过原型链最顶端的Object.prototype对象)。这意味着为了让一个类继承另一个类,只需将子类的prototype设置为指向超类的一个实例即可。这与其他语言中的继承迥然不同,可能会让人非常费解,而且有违直觉。
为了让Author继承Person,必须手工将Author的prototype设置为Person的一个实例。最后一个步骤是将prototype的constructor属性重设为Author(因为把prototype属性设置为Person的实例时,其constructor属性被抹除了)。
尽管本例中为实现继承需要额外使用三行代码,但是创建这个新的子类的实例与创建Person的实例没什么不同:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var author = []; 2 author[0] = new Author('Dustin DIaz', ['Javascript Patterns']) 3 author[1] = new Author('Ross Harmes', ['Javascript Patterns']) 4 5 author[1].getName(); 6 author[1].getBooks();
由此可见,类式继承的所有复杂性只限于类的声明,创建新实例的过程仍然很简单。
4.2.2 extend函数
为了简化类的声明,可以吧派生子类的整个过程包装在一个名为extend的函数中。它的作用与其他语言中的extend关键字类似,即基于一个给定的类结构创建一个新的类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Extend function*/ 2 function extend(subClass, superClass) { 3 var F = function() {}; 4 F.prototype = superClass.prototype; 5 subClass.prototype = new F(); 6 subClass.prototype.constructor = subClass; 7 }
这个函数所做的事与先前我们手工做的一样。他设置了prototype,然后再将其contructor重设为恰当的值。作为一项改进,它添加了一个空函数F,并将用它创建的一个对象实例插入原型链中。这样做可以避免创建超类的新实例,因为它可能会比较庞大,而且有时超类的构造函数有一些副作用,或者会执行一些需要进行大量计算的任务。
使用extend函数后,前面那个Person/Author例子变成了这一个样子:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Class Person */ 2 function Person(name) { 3 this.name = name; 4 } 5 Person.prototype.getName = function () { 6 return this.name; 7 } 8 9 /* 10 * 不使用extend函数继承 11 * */ 12 /* Class Author */ 13 function Author(name, books) { 14 Person.call(this, name); 15 this.books = books; 16 } 17 Author.prototype = new Person(); 18 Author.prototype.constructor = Author; 19 Author.prototype.getBooks = function () { 20 return this.books; 21 } 22 23 /* 24 * 使用extend函数继承 25 * */ 26 function extend(subClass, superClass) { 27 var F = function() {}; 28 F.prototype = superClass.prototype; 29 subClass.prototype = new F(); 30 subClass.prototype.constructor = subClass; 31 } 32 /* Class Author */ 33 function Author(name, books) { 34 Person.call(this, name) 35 this.books = books; 36 } 37 extend(Author, Person); 38 39 Author.prototype.getBooks = function () { 40 return this.books; 41 }
本例不像先前那样手工设置prototype和constructor属性,而是通过在类声明之后(在prototype添加任何方法之前)立即调用extend函数来达到同样的目的。唯一的问题时超类(Person)的名称被固化在了Author类的声明之中。更好的做法是像下面这样用一种更具普适性的方式来引用父类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Extend function, improved */ 2 function extend(subClass, superClass) { 3 var F = function() {}; 4 F.prototype = superClass.prototype; 5 subClass.prototype = new F(); 6 subClass.prototype.constructor = subClass; 7 8 subClass.superclass = superClass.prototype; 9 if( superClass.prototype == Object.prototype.constructor) { 10 superClass.prototype.constructor = superClass; 11 } 12 }
这个版本要长一点,他提供了superclass属性,这个属性可以用来弱化Author与Person之间的耦合。该函数的前面4行与前一版本相同。它的最后3行代码则用来确保超类的constructor属性已被正确设置(即使超类就是Object类本身),在用这个新的superClass属性调用超类的构造函数时这个问题很重要:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Class Author */ 2 function Author(name, books) { 3 Author.superclass.constructor.call(this, name); 4 this.books = books; 5 } 6 extend(Author, Person); 7 8 Author.prototype.getBooks = function() { 9 return this.book; 10 }
有了superclass属性,就可以直接调用超类中的方法。这在既要重定义超类的某个方法而又想访问其在超类中的实现时可以派上用场。例如,为了用一个新的getName方法重新定义Person类中的同名方法,你可以先用Author.superclass.getName获得作者的名字,然后在此基础上添加其他信息:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 Author.prototype.getName = function() { 2 var name = Author.superclass.getName.call(this); 3 return name + ', Author of ' + this.getBooks().join(',') ; 4 };
4.3 原型式继承
原型式继承与类式继承截然不同。我们发现在谈到它的时候,最好忘掉自己关于类和实例的一切知识,只从对象的角度思考。用基于类的办法来创建对象包括两个步骤:首先,用一个类的声明定义对象的结构;第二,实例化该类已创建一个新对象。用这种方式创建的对象都有一套该类的所有实例属性的副本。每一个实例方法都只存在一份,但每一个对象都有一个指向它的链接。
使用原型链继承时,并不需要用类来定义对象的结构,只需直接创建一个对象即可,这个对象随后可以被新的对象重用。这得益于原型链查找的机制。该对象被称为原型对象(protottype object),这是因为他为其他对象应有的模样提供了一个原型。这正是原型链这个名称的由来。
下面我们将使用原型式继承来重新设计Person和Author:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Person Prototype Object */ 2 3 var Person = { 4 name: 'default name', 5 getName: function() { 6 return this.name; 7 } 8 }
这里并没有使用一个名为Person的构造函数来定义类的结构,Person现在是一个对象字面量。它是所要创建的其它各种类Person对象的原型对象。其中定义了所有类Person对象都要具备的属性和方法,并为他们提供默认值。方法的默认值可能不会被改变,而属性的默认值一般都会被改变:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var reader = clone(Person); 2 alert(reader.getName()); // This will output 'default name'. 3 reader.name = 'John Smith'; 4 alert(reader.getName()); // This.will output 'John Smith'.
clone函数(稍后再4.3.2节中会详细说明)可以用来创建新的类Person对象。他会创建一个空对象,而该对象的原型对象被设置成为Person。这意味着在这个新对象中查找某个方法或属性时,如果找不到,那么查找过程会在其他原型对象中继续进行。
你不必为创建Author而定义一个Person的子类,只要执行一次克隆即可:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Author Prototype Object */ 2 3 var Author = clone(Person)' 4 Author.books = []; //Default value; 5 Author.getBooks = function() { 6 return this.books; 7 }
然后你可以重定义该克隆中的方法和属性。可以修改在Person中提供的默认值,也可以添加新的属性和方法。这样一来创建了一个新的原型对象,你可以将其用于创建新的类Author对象:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var author = []; 2 3 author[0] = clone(Author); 4 author[0].name = 'Dustin Diaz'; 5 author[0].books = ['javascript design patterns']; 6 7 author[1] = clone(Author); 8 author[1].name = 'ross harmes'; 9 author[1].books = ['javascript design patterns']; 10 11 author[1].getName(); 12 author[1].getBooks();
4.3.1 对继承而来的成员的读和写的不对等性
前面说过,为了有效的使用原型继承,你必须忘记有关类式继承的一切。这里就是一个例子。在类式继承中,Author的每一个实例都有一份自己的books数组副本。你可以用代码author[1].books.push('New Book Title')为其添加元素。但是对于使用原型式继承方式创建的类Author对象来说,由于原型链接的工作方式,这种做法并非一开始就能行的通。一个克隆并非原型对象的一份完全独立的副本,他只是一个以那个对象为原型对象的空对象而已。克隆刚被创建时,author[1].name其实是一个返指最初的Person.name的链接。对于从原型对象继承而来的成员,其读和写具有内在的不对等性。在读取author[1].name的值时,如果你还没有直接为author[1]实例定义name属性的话,那么所得到的是其原型对象的同名属性值。而在写入author[1].name的值时,你是在直接为author[1]对象定义一个新属性。
下面这个示例显示了这种不对等性:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var authorClone = clone(Author); 2 alert(authorClone.name); 3 4 authorClone.name = 'New name'; 5 6 alert(authorClone.name); 7 8 authorClone.books.push('new book'); 9 authorClone.books = []; 10 authorClone.books.push('new book');
这也说明了为什么必须为通过引用传递的数据类型的属性创建新副本。在上面的例子中,像authorClone.books数据添加新元素实际上是把这个元素添加到Author.books数组中。这可不是什么好事,因为你对那个值得修改不仅会影响到Author,而且会影响到所有继承了Author但还未改写那个属性的默认值的对象。在改变所有那些数组和对象的成员之前,必须先为其创建新的副本。稍不留神,你可能会忘记这个问题并改动原型对象的值。这种错误必须尽量避免,因为这类问题调试起来非常费时。在这类场合中,可以使用hasOwnProperty方法来区分对象的实际成员和它继承而来的成员。
有时原型对象自己也包含有子对象。如果想覆盖其子对象中的一个属性值,你不得不重新创建整个子对象。这可以通过将该子对象设置为一个空对象字面量,然后对其进行重塑而办到。但这意味着克隆出来的对象必须知道其原型对象的每一个子对象的确切结构和默认值。为了尽量弱化对象之间的耦合,任何复杂的子对象都应该使用方法来创建:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var CompoundObject = { 2 string1: 'default value', 3 childObject: { 4 bool: true, 5 num: 10 6 } 7 } 8 9 var compoundObjectClone = clone(CompoundObject); 10 11 // Bad! Changes the value of CompoundObject.childObject.num. 12 compoundObjectClone.childObject.num = 5; 13 14 // Better. Creates a new object, but compoundObject must know the structure of that object,and the defaults. This makes CompoundObject and compoundObjectClone tightly coupled 15 compoundObjectClone.childObject = { 16 bool: true, 17 num: 5 18 }
在这个例子中,为compoundObjectClone对象新添加一个childObject属性,并修改了他所指向的对象的num属性。问题在于compoundObjectClone必须知道childObject具有两个默认值分别为true和10的属性。更好的办法是用一个工厂方法来创建childObject:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var CompoundObject = {}; 2 CompoundObject.string1 = 'default value'; 3 CompoundObject.createChildObject = function () { 4 return { 5 bool: true, 6 num: 10 7 } 8 }; 9 CompoundObject.childObject = CompoundObject.createChildObject(); 10 11 var compoundObjectClone = clone(CompoundObject); 12 compoundObjectClone.childObject = CompoundObject.createChildObject(); 13 compoundObjectClone.childObject.num = 5;
4.3.2 clone函数
在前面的例子中用来创建克隆对象的其妙函数究竟是个什么样子?答案如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Clone function. */ 2 3 function clone(object) { 4 function F() {} 5 F.prototype = object; 6 return new F; // new F => new F(); 7 }
clone函数首先创建了一个新的空函数F,然后将F的prototype属性设置为作为参数object传入的原型对象。由此可以体会到JavaScript最初设计者的用意。prototype属性就是用来指向原型对象的,通过原型链接机制,它提供了到所有继承而来的成员的链接。该函数最后通过把new运算符作用于F创建一个新对象,然后把这个新对象作为返回值返回。函数所返回的这个克隆结果是一个以给定对象为原型对象的空对象。
4.4 类式继承和原型式继承的对比
类式继承和原型式继承是大相径庭的两种继承范型,它们生成的对象也有不同的行为方式。两种继承范型都各有其优缺点,为了判断在特定场合下应该是用哪一种,你需要对此有所了解。
包括JavaScript程序员在内的整个程序员群体对类式继承都比较熟悉。几乎所有用面向对象方式编写的JavaScript代码中都用到了这种继承范型。如果你设计的是一个公众人使用的API,或者可能会有不熟悉原型式继承的其他程序员基于你的代码进行设计,那么最好是使用类式继承。在各种流行语言中只有JavaScript使用原型式继承,因此可能很多人从来没有用过这种继承。而对象具有到自己的原型对象的反向链接,这也是一个令人困惑的机制。那些没有完全理解原型式继承的程序员会把他视为某种反向继承,即父类继承子类。即使事实并非如此,原型式继承仍然会是一个令人费解的话题。但是,因为JavaScript中的类式继承仅仅是对真正基于类的继承的一种模仿,所以那些高级的JavaScript程序员总有一天需要懂得原型式继承的工作机制。有人认为隐瞒这一个事实其实是弊大于利。
原型式继承更能节约内存。原型链读取成员的方式使得所有克隆出来的对象都共享每个属性和方法的唯一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法时,情况才会有所改变。与此相比,在类似继承方式中创建的每一个对象在内存中都有自己的一套属性(和私用方法)的副本。原型式继承在这方面的节约效果很突出。这种继承也比类式继承显得更为简练,他只用到了一个clone函数,不像后者那样需要为每一个想扩展的类写上好几行像superClass.call(this, arg)和SubClass.prototype = new SuperClass这样的晦涩代码(当然,这几行代码中的一部分也可以被塞到extend函数中)。不要把原型式的简洁看作是简陋,他的力量蕴含在其简洁性之中。
该使用类式继承还是原型式继承也许主要是取决于你更喜欢哪种范型。有些人似乎天生就容易被原型式继承的间接性吸引,而另一些人却对更面熟的类式继承情有独钟。每一种设计模式中都可以使用这两种继承范型。为了便于理解,在讲述后面的设计模式时我们主要使用类式继承,但这两种继承范型在整本书中都是可以互换使用的。
4.5 继承与封装
在本章中到目前为止基本没提到过封装对继承的影响。从现有的类派生出一个子类时,只有公用和特权成员会被承袭下来。这与其他面向对象语言中的情况类似。以Java为例,其子类就无法访问到父类的私用方法:为了将一个方法遗传给子类,必须定义它时使用关键字protected。
由于这个原因,门户大开型类是最合适与派生子类的。它们的所有成员都是公开的,因此可以被遗传给子类。如果某个成员需要稍加隐藏,你可以使用下划线符号规范。
在派生具有真正的私用成员的类时,因为其特权方法是公用的,所以他们会被遗传下来。籍此可以在子类中间接访问父类的私用属性,但子类自身的实例方法都不能直接访问这些私用属性。父类的私用成员只能通过这些既有的特权方法进行访问,你不能在子类中添加能够直接访问它们的新的特权方法。
4.6 掺元类
有一种重用代码的方法不需要用到严格的继承。如果想把一个函数用到多个类中,可以通过扩充(augmentation)的方式让这些类共享该函数。其实际做法大体为:先创建一个包含各种通用的方法类,然后再用它扩充其他的类。这种包含通用方法的类成为称为掺元类(mixin class)。他们通常不会被实例化或直接调用。其存在的目的只是想其他类提供自己的方法。示例如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Mixin class. */ 2 3 var Mixin = function () {}; 4 Mixin.prototype = { 5 serialize: function () { 6 var output = []; 7 for (key in this) { 8 output.push(key + ':' + this[key]); 9 } 10 return output.join(', ') 11 } 12 }
这个Mixin类只有一个名为serialize的方法。这个方法遍访this对象的所有成员并将它们的值组织为一个字符串输出。这种方法可能在许多不同类型的类中都会用到,但没有必要让这些类都继承Mixin,把这个方法的代码复制到这些类中也并不明智。最好还是用augment函数把这个方法添加到每一个需要它的类中:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 augment(Author, Mixin); 2 3 var author = new Author('Ross Harmes', ['javascript design patterns']); 4 var serializedString = author.serialize();
在此我们用Mixin类中的所有方法扩充了Author类。Author类的实例现在就可以调用serialize方法了。这可以被视为多亲继承(multiple inheritance)在JavaScript中的一种实现方式。c++和python这类语言允许子类继承多个超类。这在JavaScript中是不允许的,因为一个对象只能拥有一个原型对象。不过,由于一个类可以用多个掺元类加以扩充,所以这实际上实现了多继承的效果。
augment函数很简单。它用一个for...in循环遍访予类(giving class)的prototype中的每一个成员,并将其添加到受类(receiving class)的prototype中。如果受类中已经存在同名成员,则跳过这个成员,转而处理下一个。受类的成员不会被写成:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Augment function。 */ 2 3 function augment(receivingClass, givingClass) { 4 for(methodName in givingClass.prototype) { 5 if (!receivingClass.prototype[methodName]) { 6 receivingClass.prototype[methodName] givingClass.prototype[methodName]; 7 } 8 } 9 }
这个函数还可以再改进一下。如果掺元类中包含多个方法,但你只想复制其中的一两个,那么上面这个版本的augment函数时无法满足需要的。下面的新版本会检查是否存在额外的可选参数,如果存在,则只复制那些名称与这些参数匹配的方法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Augment function。 */ 2 3 function augment(receivingClass, givingClass) { 4 if (arguments[2]) { 5 for (var i=2, len = arguments.length; i<len; i++) { 6 receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; 7 } 8 } else { 9 for(methodName in givingClass.prototype) { 10 if (!receivingClass.prototype[methodName]) { 11 receivingClass.prototype[methodName] givingClass.prototype[methodName]; 12 } 13 } 14 } 15 }
现在就可以用augment(Author, Mixin, 'serialize');这条语句来达到只为Author类添加一个serialize方法的目的了。如果添加更多的方法,只要把它们的名称作为参数传入即可。
用一些方法来扩充一个类有时比让这个类继承另一个类更合适。这是一种避免出现重复性代码的轻便的解决办法。不过适合这种方案的场合并不是很多。只有那些通用到足以使其在彼此大不相同的各种类中都能派上用场的方法才适合与共享(要是那些类彼此的差异不是那么大,那么普通的继承往往更合适)。
4.7 示例: 就地编辑
我们将提供这个示例的三种解决方案,他们分别演示了类式继承、原型式继承、掺元类的用法。假设你的任务是编写一个用于创建和管理就地编辑的可重用的模块化API(就地编辑(edit-in-place)是指网页上的一段普通文本被点击后就变成一个配有一些按钮的表单域,以便用户就地对这段文本进行编辑)。使用这个API,用户应该能够为对象分配一个唯一的ID值,能够为他提供一个默认值,并且能够指定在页面上的目标位置。用户还应该在任何时候都可以访问到这个域的当前值,并且可以选择具体使用的编辑域(比如多行文本框或单行文本框)。
4.7.1 类式继承解决方案
我们先用类式继承创建这个API:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceFileld class. */ 2 function EditInPlaceField(id, parent, value) { 3 this.id = id; 4 this.value = value || 'default value'; 5 this.parentElement = parent; 6 7 this.createElements(this.id); 8 this.attachEvents(); 9 } 10 EditInPlaceField.prototype = { 11 createElements: function (id) { 12 this.containerElement = document.createElement('div'); 13 this.parentElement.appendChild(this.containerElement); 14 15 this.staticElement = document.createElement('span'); 16 this.containerElement.appendChild(this.staticElement); 17 this.staticElement.innerHTML = this.value; 18 19 this.fieldElement = document.createElement('input'); 20 thiss.fieldElement.type = 'text'; 21 this.fieldElement.value = this.value; 22 this.containerElement.appendChild(this.fieldElement); 23 24 this.saveButton = document.createElement('input'); 25 this.saveButton.type = 'button'; 26 this.saveButton.value = 'Save'; 27 this.containerElement.appendChild(this.saveButton); 28 29 this.cancelButton = document.createElement('input'); 30 this.cancelButton.type = 'button'; 31 this.cancelButton.value = 'Cancel'; 32 this.containerElement.appendChild(this.cancelButton); 33 this.convertToText(); 34 }, 35 attachEvents: function () { 36 var that = this; 37 addEvent(this.staticElement, 'click', function () { 38 that.convertToRditable(); 39 }); 40 addEvent(this.saveButton, 'click', function () { 41 that.save(); 42 }); 43 addEvent(this.cancelButton, 'click', function () { 44 that.cancel(); 45 }) 46 }, 47 convertToRditable: function () { 48 this.staticElement.style.display = 'none'; 49 this.fieldElement.style.display = 'inline'; 50 this.saveButton.style.display = 'inline'; 51 this.cancelButton.style.display = 'inline'; 52 53 this.setValue(this.value); 54 }, 55 save: function () { 56 this.value = this.getValue(); 57 var that = this; 58 var callback = { 59 success: function () { 60 that.convertToText(); 61 }, 62 failure: function () { 63 alert('Error saving value'); 64 } 65 }; 66 ajaxRequest('Get', 'save.php?id=' + this.id + '&value=' + this.value, callback); 67 }, 68 cancel: function () { 69 this.convertToText(); 70 }, 71 convertToText: function () { 72 this.fieldElement.style.display = 'none'; 73 this.saveButton.style.display = 'none'; 74 this.cancelButton.style.display = 'none'; 75 this.staticElement.style.display = 'inline'; 76 77 this.setValue(); 78 }, 79 setValue: function () { 80 this.fieldElement.value = value; 81 this.staticElement.innerHTML = value; 82 }, 83 getValue: function () { 84 return this.fieldElement.value; 85 } 86 }; 87 88 //要创建一个就地编辑域, 只需要实例化这个类即可: 89 var titleClassical = new EditInPlaceField('titleClassical', $('doc'), 'Title Here'); 90 var currentTitleText = titleClassical.getValue();
上述语句创建了EditInPlaceField类(稍后我们将从它派生出新类)的一个实例,它用一个span标签显示文字,并用一个单行文本框作为文字的编辑区。它还具有一些配置方法(createElements、addachRvents)、一些用于转换和保存的内部方法(convertToEditable、 save、 cancel、 converToText)以及一对取值器和赋值器(getValue、setValue)。如果是实际使用中的代码,你最好为其中的每一个HTML元素指定一个特定的class属性值,一边用css来设置他们的样式。为简洁起见,我们并没有这样做。
接下来我们要创建一个使用多行文本框而不是单行文本框的类。这个EditInPlaceArea类与EditInplaceField类有很多共同之处,所以我们将前者作为后者的子类处理,以免编写重复性的代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceArea class. */ 2 function EditInPlaceArea(id, parent, value) { 3 EditInPlaceArea.superclass.constructor.call(this, id, parent, value); 4 } 5 extend(EditInPlaceArea, EditInPlaceField); 6 7 // Override certain methods. 8 EditInPlaceArea.prototype.createElements = function(id) { 9 this.containerElement = document.createElement('div'); 10 this.parentElement.appendChild(this.containerElement); 11 12 this.staticElement = document.createElement('p'); 13 this.containerElement.appendChild(this.staticElement); 14 this.staticElement.innerHTML = this.value; 15 16 this.fieldElement = document.createElement('textarea'); 17 this.fieldElement.value = this.value; 18 this.containerElement.appendChild(this.fieldElement); 19 20 this.saveButton = document.createElement('input'); 21 this.saveButton.type = 'button'; 22 this.saveButton.value = 'Save'; 23 this.containerElement.appendChild(this.saveButton); 24 25 this.cancelButton = document.createElement('input'); 26 this.cancelButton.type = 'button'; 27 this.cancelButton.value = 'Cancel'; 28 this.containerElement.appendChild(this.cancelButton); 29 this.convertToText(); 30 }; 31 EditInPlaceArea.prototype.convertToEditable = function () { 32 this.staticElement.style.display = 'none'; 33 this.fieldElement.style.display = 'block'; 34 this.saveButton.style.display = 'inline'; 35 this.cancelButton.style.display = 'inline'; 36 37 this.setValue(this.value); 38 }; 39 EditInPlaceArea.prototype.convertToText = function () { 40 this.fieldElement.style.display = 'none'; 41 this.saveButton.style.display = 'none'; 42 this.cancelButton.style.display = 'none'; 43 this.staticElement.style.display = 'block'; 44 45 this.setValue(this.value); 46 };
上述代码中用extend函数实现子类的派生,然后在子类中重定义了父类中的一些方法,已体现出二者的差别。这个新类用一个多行文本框取代父类中的单行文本框,用一个P标签取代父类中的span标签。
类式继承技术用在这个案例中看起来很理想。从EditInPlaceField派生子类只是举手之劳,用不了多少代码。要体现子类与父类的差别,只需在子类吃那个新定义一些父类的方法或添加一些新方法即可。我们还可以把这种就地编辑域与别的输出方式关联起来,为此需要另外创建一个子类,并在其中重定义父类的save方法。因为此例中类之间的差异很小,所以这种严格的继承是一种理想的选择。
4.7.2 原型式继承解决方案
尽管类式继承和原型式继承有根本性的差别,但从改用原型式继承完成这个任务的过程中你会发现这两种方案的最终代码非常相似:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceField Object. */ 2 3 var EditInPlaceField = { 4 configure: function (id, parent, value) { 5 this.id = id; 6 this.value = value || 'default value'; 7 this.parentElement = parent; 8 9 this.createElements(this.id); 10 this.attachEvents(); 11 }, 12 createElements: function (id) { 13 this.containerElement = document.createElement('div'); 14 this.parentElement.appendChild(this.containerElement); 15 16 this.staticElement = document.createElement('span'); 17 this.containerElement.appendChild(this.staticElement); 18 this.staticElement.innerHTML = this.value; 19 20 this.fieldElement = document.createElement('input'); 21 thiss.fieldElement.type = 'text'; 22 this.fieldElement.value = this.value; 23 this.containerElement.appendChild(this.fieldElement); 24 25 this.saveButton = document.createElement('input'); 26 this.saveButton.type = 'button'; 27 this.saveButton.value = 'Save'; 28 this.containerElement.appendChild(this.saveButton); 29 30 this.cancelButton = document.createElement('input'); 31 this.cancelButton.type = 'button'; 32 this.cancelButton.value = 'Cancel'; 33 this.containerElement.appendChild(this.cancelButton); 34 this.convertToText(); 35 }, 36 attachEvents: function () { 37 var that = this; 38 addEvent(this.staticElement, 'click', function () { 39 that.convertToRditable(); 40 }); 41 addEvent(this.saveButton, 'click', function () { 42 that.save(); 43 }); 44 addEvent(this.cancelButton, 'click', function () { 45 that.cancel(); 46 }) 47 }, 48 convertToEditable: function () { 49 this.staticElement.style.display = 'none'; 50 this.fieldElement.style.display = 'inline'; 51 this.saveButton.style.display = 'inline'; 52 this.cancelButton.style.display = 'inline'; 53 54 this.setValue(this.value); 55 }, 56 save: function () { 57 this.value = this.getValue(); 58 var that = this; 59 var callback = { 60 success: function () { 61 that.convertToText(); 62 }, 63 failure: function () { 64 alert('Error saving value'); 65 } 66 }; 67 ajaxRequest('Get', 'save.php?id=' + this.id + '&value=' + this.value, callback); 68 }, 69 cancel: function () { 70 this.convertToText(); 71 }, 72 convertToText: function () { 73 this.fieldElement.style.display = 'none'; 74 this.saveButton.style.display = 'none'; 75 this.cancelButton.style.display = 'none'; 76 this.staticElement.style.display = 'inline'; 77 78 this.setValue(); 79 }, 80 setValue: function () { 81 this.fieldElement.value = value; 82 this.staticElement.innerHTML = value; 83 }, 84 getValue: function () { 85 return this.fieldElement.value; 86 } 87 };
上述代码中并没有创建类,而是创建了一个对象。原型式继承不使用构造函数,所以类式继承方案中的构造函数中的代码在这个方案中被移到了一个名为configure的方法中。除此之外,这个方案中的代码与前一个方案几乎一摸一样。根据EditInPlaceField这个原型对象创建新对象的方式与类进行实例化大不相同。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var titlePrototypeal = clone(EditInPlaceField); 2 titlePrototypeal .configure('titlePrototypeal',$('doc'), 'Title Here'); 3 var currentTitleText = titlePrototypeal .getValue();
这里不再使用new运算符,而是使用clone韩束来创建一个对象副本,然后在对这个副本进行配置。此后就可以像对待前面的titleClassical对象一样与titlePrototypal这个对象打交道了。这两个对象基本没有什么分别,你可以用同样的API来处理它们。
创建这个对象的子对象也需要用到clone函数:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceArea object. */ 2 var EditInPlaceArea = clone(EditInPlaceField); 3 // Override certain methods. 4 EditInPlaceArea.createElements = function(id) { 5 this.containerElement = document.createElement('div'); 6 this.parentElement.appendChild(this.containerElement); 7 8 this.staticElement = document.createElement('p'); 9 this.containerElement.appendChild(this.staticElement); 10 this.staticElement.innerHTML = this.value; 11 12 this.fieldElement = document.createElement('textarea'); 13 this.fieldElement.value = this.value; 14 this.containerElement.appendChild(this.fieldElement); 15 16 this.saveButton = document.createElement('input'); 17 this.saveButton.type = 'button'; 18 this.saveButton.value = 'Save'; 19 this.containerElement.appendChild(this.saveButton); 20 21 this.cancelButton = document.createElement('input'); 22 this.cancelButton.type = 'button'; 23 this.cancelButton.value = 'Cancel'; 24 this.containerElement.appendChild(this.cancelButton); 25 this.convertToText(); 26 }; 27 EditInPlaceArea.convertToEditable = function () { 28 this.staticElement.style.display = 'none'; 29 this.fieldElement.style.display = 'block'; 30 this.saveButton.style.display = 'inline'; 31 this.cancelButton.style.display = 'inline'; 32 33 this.setValue(this.value); 34 }; 35 EditInPlaceArea.convertToText = function () { 36 this.fieldElement.style.display = 'none'; 37 this.saveButton.style.display = 'none'; 38 this.cancelButton.style.display = 'none'; 39 this.staticElement.style.display = 'block'; 40 41 this.setValue(this.value); 42 };
上述代码只是先创建EditInPlaceField对象的一个副本,然后改写其中的一些方法。EditInPlaceArea这个原型对象可以向第一个原型对象一样使用和克隆。实际上,你还可以如法炮制,在此基础上创建新的原型对象,为此只需对其进行克隆并在新对象中进行一些修改即可。
原型式继承技术看来很适合这个案例,其原因和类式继承类似。这两种方案唯一的差别在于创建类(对象)以及从它们派生子对象(实例)的方式。大多数代码(包括所有的方法)在两种方案中完全相同。由此可见从其中一种范型转到另一种范型有多么容易。不是所有的转变都有这样轻松,特别是在涉及具有大量数组或对象类型的成员的类和对象时,但通常你只需要在语法方面进行一些调整即可。
在这个案例中,原型式继承相对于类式继承并未表现出什么优越性。其中的对象都没有用到多少默认值,因此新方案的使用没有节省什么内存。我们很难说哪种方案更好,他们在此例中难分伯仲。
4.7.3 掺元类解决方案
这次我们要使用掺元类来处理这个问题。我们首先创建一个包含了所有要共享的方法的掺元类,然后再创建一个新类,并使用augment函数来让这个新类共享到那些方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* Mixin class for the edit-in-place methods. */ 2 3 var EditInPlaceMixin = function () {}; 4 EditInPlaceMixin.prototype = { 5 createElements: function (id) { 6 this.containerElement = document.createElement('div'); 7 this.parentElement.appendChild(this.containerElement); 8 9 this.staticElement = document.createElement('span'); 10 this.containerElement.appendChild(this.staticElement); 11 this.staticElement.innerHTML = this.value; 12 13 this.fieldElement = document.createElement('input'); 14 thiss.fieldElement.type = 'text'; 15 this.fieldElement.value = this.value; 16 this.containerElement.appendChild(this.fieldElement); 17 18 this.saveButton = document.createElement('input'); 19 this.saveButton.type = 'button'; 20 this.saveButton.value = 'Save'; 21 this.containerElement.appendChild(this.saveButton); 22 23 this.cancelButton = document.createElement('input'); 24 this.cancelButton.type = 'button'; 25 this.cancelButton.value = 'Cancel'; 26 this.containerElement.appendChild(this.cancelButton); 27 this.convertToText(); 28 }, 29 attachEvents: function () { 30 var that = this; 31 addEvent(this.staticElement, 'click', function () { 32 that.convertToRditable(); 33 }); 34 addEvent(this.saveButton, 'click', function () { 35 that.save(); 36 }); 37 addEvent(this.cancelButton, 'click', function () { 38 that.cancel(); 39 }) 40 }, 41 convertToEditable: function () { 42 this.staticElement.style.display = 'none'; 43 this.fieldElement.style.display = 'inline'; 44 this.saveButton.style.display = 'inline'; 45 this.cancelButton.style.display = 'inline'; 46 47 this.setValue(this.value); 48 }, 49 save: function () { 50 this.value = this.getValue(); 51 var that = this; 52 var callback = { 53 success: function () { 54 that.convertToText(); 55 }, 56 failure: function () { 57 alert('Error saving value'); 58 } 59 }; 60 ajaxRequest('Get', 'save.php?id=' + this.id + '&value=' + this.value, callback); 61 }, 62 cancel: function () { 63 this.convertToText(); 64 }, 65 convertToText: function () { 66 this.fieldElement.style.display = 'none'; 67 this.saveButton.style.display = 'none'; 68 this.cancelButton.style.display = 'none'; 69 this.staticElement.style.display = 'inline'; 70 71 this.setValue(); 72 }, 73 setValue: function () { 74 this.fieldElement.value = value; 75 this.staticElement.innerHTML = value; 76 }, 77 getValue: function () { 78 return this.fieldElement.value; 79 } 80 };
这个掺元类之定义了一些方法。要创建一个职能类,,需要先创建一个构造函数,然后再调用augment:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceField class. */ 2 3 function EditInPlaceField(id, parent, vlaue) { 4 this.id = id; 5 this.value = value || 'default value'; 6 this.parentElement = parent; 7 8 this.createElements(this.id); 9 this.attachEvents(); 10 }; 11 augment(EditInPlaceField, EditInPlaceMixin);
随后即可像类式继承方案中那样对这个类进行实例化。要创建使用多行文本框的类,你不是从EditInPlaceField派生子类,而是另行创建一个新类(的构造函数)并用同样的掺元类扩充它。但是在扩充这个新类之前,先要为其定义一些方法。由于它们是在扩充之前定义的,所以不会被覆盖。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* EditInPlaceArea class. */ 2 3 function EditInPlaceArea() { 4 this.id = id; 5 this.value = value || 'default value'; 6 this.parentElement = parent; 7 8 this.createElements(this.id); 9 this.attachEvents(); 10 } 11 12 // Add certain methods so that augment won`t include them. 13 14 EditInPlaceArea.prototype.createElements = function(id) { 15 this.containerElement = document.createElement('div'); 16 this.parentElement.appendChild(this.containerElement); 17 18 this.staticElement = document.createElement('p'); 19 this.containerElement.appendChild(this.staticElement); 20 this.staticElement.innerHTML = this.value; 21 22 this.fieldElement = document.createElement('textarea'); 23 this.fieldElement.value = this.value; 24 this.containerElement.appendChild(this.fieldElement); 25 26 this.saveButton = document.createElement('input'); 27 this.saveButton.type = 'button'; 28 this.saveButton.value = 'Save'; 29 this.containerElement.appendChild(this.saveButton); 30 31 this.cancelButton = document.createElement('input'); 32 this.cancelButton.type = 'button'; 33 this.cancelButton.value = 'Cancel'; 34 this.containerElement.appendChild(this.cancelButton); 35 this.convertToText(); 36 }; 37 EditInPlaceArea.prototype.convertToEditable = function () { 38 this.staticElement.style.display = 'none'; 39 this.fieldElement.style.display = 'block'; 40 this.saveButton.style.display = 'inline'; 41 this.cancelButton.style.display = 'inline'; 42 43 this.setValue(this.value); 44 }; 45 EditInPlaceArea.prototype.convertToText = function () { 46 this.fieldElement.style.display = 'none'; 47 this.saveButton.style.display = 'none'; 48 this.cancelButton.style.display = 'none'; 49 this.staticElement.style.display = 'block'; 50 51 this.setValue(this.value); 52 }; 53 augment(EditInPlaceArea, EditInPlaceMixin);
这个案例中可以使用掺元类技术,但是这种方案不如前面两种。虽然使用这些技术最终创建的对象大体相同,但是从条理性的角度来看,严格的继承方案比扩充方案更加清楚。掺元类非常适合于组织那些彼此迥然不同的类所共享的方法。但本例中的掺元类却是在为两个非常相似的类提供所有的方法。前面两个方案中的代码要更容易维护一些,因为开发这一眼就能看出那些方法的来源以及类和对象的组织方式。
实现可以用在各类对象中的通用方法的共享才是掺元类如鱼得水的领域。这方面的例子包括将对象序列化为字符串形式的方法和输出对象的状态以供调式只用的方法。掺元类还可以用来模仿其他面向对象的语言中的枚举或迭代器。
4.8 继承的适用场合
继承会使代码变得更加复杂、更难被JavaScript新手理解,所以应该用在其带来的好处胜过缺点的那些场合。它的主要好处表现在代码的重用方面。通过建立类或对象之间的继承关系,有些方法我们只需要定义一次即可。同样,如果需要修改这些方法或排查其中的错误,那么由于其定义只出现在一个位置,所以非常有利于节省时间和精力。
各种继承范型都有自己的优缺点。在内存效率比较重要的场合原型式继承(及clone函数)是最佳的选择。如果与对象打交道的都是些熟悉其他面向对象语言中的继承机制的程序员,那么是用类式继承(及extend函数)。这两种方法都适合于类间差异较小的类层次体系。如果类之间的差异较大,那么用掺元类中的方法来扩充这些类往往是一种更合理的选择。
比较简单的JavaScript程序很少需要用到这种程度的抽象。只有那些有许多程序员参与的大型项目才需要这种代码组织手段。
4.9 小结
本章讨论了继承的优点和缺点,以及让一个类和对象继承另一个类或对象的三种方法。类式继承试图模仿C++和Java等面向对象语言种类的继承方式。它最适合于内存效率要求不高或程序员不熟悉那种不太知名的原型式继承场合。子类派生过程中大多数令人困惑的步骤可以用extend函数封装起来。
原型式继承的机制实现创建一些对象然后再对其进行克隆,从而得到创建子类和实例的等效结果。一旦你明白了其中的道理,这种继承方式用起来会比较顺手,而且用这种发放创建的对象往往有较高的内存效率,这是因为他们会共享那些未被该写的属性和方法。那些包含着数组或对象类型的成员的对象的克隆会有一些麻烦之处,但这个问题可以通过使用一个方法来设置那些属性的默认值加以解决。创建一个克隆对象的所有事宜由clone函数负责处理。
掺元类提供了一条既能让对象和类共享一些方法又不需要让它们结成父子关系的途径。如果你想让各种彼此有着较大差异的类共享一些通用方法,那么这正是掺元类的用武之地。augment函数允许你选择共享掺元类中的全部方法还是部分方法。
使用这三种技术可以创建出复杂的对象层次体系,其简练行堪与任何别的面向对象语言媲美。对于编程新手来说,JavaScript中的继承不那么好懂或直观。它是一种通过研究这种语言的低层特性而发展起来的高级技术,但是它可以通过使用几个便利函数而得以简化。这种技术非常适合用于创建供其他程序员使用的API。