继承
下面将描述创建子类的各种技术以及他们的适用场合。
为什么需要继承
一般来说,在设计类的时候,我们希望减少重复性的代码,并且弱化对象间的耦合。使用继承符合前一个设计原则的需要。
类式继承
通过用函数来声明类、用关键字new来创建实例,下面是一个简单的类声明:
/* Class Person */
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
首先创建构造函数,按习惯,其名称就是类名,首字母应该大写。创建类的实例,只需结合关键字new调用构造函数既可:
var Lyf = new Person("Lyf");
Lyf.getName();
原型链
创建继承 Person的类要复杂点:
/* Class Author */
function Author(name, books){
Person.call(this, name);
this.books = books; // 添加一个属性
}
Author.prototype = new Person(); // 创建一下原型链
Author.prototype.constructor = Author; // 给Author创建constructor属性
Author.prototype.getBooks = function(){ // 添加一个方法
return this.books;
} ;
在访问对象的某个成员时,如果这个成员未见于当前对象,那么JavaScript会在原型对象中查找,如果在这个对象中也没有找到,那么JavaScript会沿着原型链向上逐一访问每个原型对象,知道找到这个成员。这意味着为了让一个类继承另一个类,只需将子类的prototype设置为指向超类的一个实例即可。
为了让Author继承Person,将Author的prototype设置为Person的一个实例,最后一个步骤将prototype的constructor属性重设为Author(因为把prototype属性设置为Person的实例后,其constructor属性被抹除了);
创建新的子类的实例与创建Person的实例没什么区别:
var author = [];
author[0] = new Auther("L", ["book1"]);
extend函数
为了简化类的声明,可以把派生子类的整个过程包装在一个名为extend的函数中,作用就是,基于一个给定的类结构创建一个新的类:
/* Extend function */
function extend(subClass, superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
使用extend函数后,前面那个Person/Author例子变成这样:
/* Class Person */
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
/* Class Author */
function Author(name, books){
Person.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function(){
return this.books;
}
这个例子不用手工设置prototype和constructor属性,但是超类的名称被固化在Author类中,更好的做法是像下面这样引用父类:
/* Extend function, improved */
function extend(subClass, superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
// 确保超类的 constructor属性被正确设置(即使超类就是Object类本身)
if(superClass.prototype.constructor == Object.prototype.constructor){
superClass.prototype.constructor = superClass;
}
}
这个版本能提供了 superclass属性,这个属性弱化Author和Person之间的耦合。
/* Class Author */
function Author(name, books){
Author.superclass.constructor.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function(){
return this.books;
};
原型式继承
基于类的办法来创建对象包括:
- 用一个类的声明定义对象的结构
- 实例化该类来创建一个新的对象
以这种方式创建的对象都有一套该类所有实例属性的副本。
使用原型继承,并不需要类来定义对象的结构,只需直接创建对象即可。该对象被称为原型对象。
下面用原型式继承来重新设计Person和Author:
/* Person Prototype Object */
var Person = {
name : "default name",
getName : function(){
return this.name;
}
};
var reader = clone(Person);
alert(reader.getName()); // 输出 "default name"
reader.name = "L";
alert(reader.getName()); // 输出 "L"
clone函数 可以用来创建新的类Person对象。它会创建一个空对象,而对象原型对象被设置成Person。
不需要为创建 Author而定义一个Person的子类,只需执行一次克隆:
/* Clone Function */
function clone(object){
function F();
F.prototype = object;
return new F();
}
/* Author Prototype Object */
var Author = clone(Person);
Author.books = []; // Default value
Author.getBooks = function(){
return this.books;
}
然后可以重定义该克隆中的方法和属性。可以修改在Person中提供的默认值,也可以添加新的属性和方法。
var author = [];
author[0] = clone(Author);
author[0].name = "Dustin Diaz";
author[0].books = ["JavaScript Design Patterns"];
有时候原型对象,自己也有子对象,如果想覆盖其子对象中的一个属性,这可以通将该子对象设置为一个空对象,然后对其进行重塑。但这意味着克隆出来的对象必须知道其原型对象的确切结构和默认值。为了弱化对象之间的耦合,任何复杂的子对象都应该使用方法来创建:
var CompoundObject = {
string1 : "default value",
childObject : {
bool : true,
num : 10
}
};
var compoundObjectClone = clone(CompoundObject);
// 不好的做法
compoundObjectClone.childObject.num = 5;
// 好的做法,创建一个新的对象,但是 compoundObject必须知道这个对象的构造和默认值。这使得 CompoundObject和compoundObjectClone耦合
compoundObjectClone.childObject = {
bool : true,
num : 5
};
更好的做法是用一个工厂方法来创建childObject:
var CompoundObject = {};
CompoundObject.string1 = "default value",
CompoundObject.createChildObject = function(){
return {
bool : true,
num : 10
}
};
CompoundObject.childObject = CompoundObject.createChildObject();
var compoundObjectClone = clone(CompoundObject);
compoundObjectClone.childObject = CompoundObject.createChildObject();
compoundObjectClone.childObject.num = 5;
掺元类
如果想把一个函数用到多个类中,可以通过扩充的方式让这些类共享该函数。
实际做法大体为: 先创建一个包含各种通用方法的类,然后再用它扩充其他类。这种包含通用方法的类,被称为掺元类(mixin class)。他们通过不会被实例化,或者直接到能调用。其存在目的就是向其他类提供自己的方法。
/* Mixin class */
var Mixin = function(){};
Mixin.prototype = {
serialize : function(){
var output = [];
for(key in this){
output.push(key + ":" + this[key]);
}
return output.join(",");
}
}
这个Mixin类只有一个serialize方法,这个方法遍历this对象的所有成员并将它们的值组织成一个字符串输出。这个方法可能在不同的类中用到,但是没必要让这些类都继承Mixin。最好是用 augment函数把这个方法添加到需要它的类中:
/* Augment function */
function augment(receivingClass, givingClass){
for(methodName in givingClass.prototype){
if(!receivingClass.prototype[methodName]){
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
}
}
如果掺元类中包含许多方法,你只想复制其中的一两个,那么这个版本的 augment函数是无法满足的。
下面这个版本会检查是否存在额外的可选参数,如果存在,则只复制那么名称与这些参数匹配的方法:
/* Augment function, improved */
function augment(receivingClass, givingClass){
if(arguments[2]){ // 只给确定的方法
for(var i = 2, len = arguments.length; i < len; i++){
receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
}
} else { // 所有的方法
for(methodName in givingClass.prototype){
if(!receivingClass.prototype[methodName]){
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
}
}
}
现在就可以用 augment(Author, Mixin, “serialize”);这一句可以达到只为Author类添加一个serialize方法的目的了。
示例:就地编辑
编写一个用于创建和管理就地编辑域的可重用的模块化API(就地编辑是指网页上一段普通文本被点击后变成一个配有一些按钮的表单域,以便用户就地对这段文本进行编辑)。
类式继承
先用类式继承创建这个API:
/* EditInPlaceField Class */
function EditInPlaceField(id, parent, value){
this.id = id;
this.value = value || "default value";
this.parentElement = parent;
this.createElements(this.id);
this.attachEvents();
}
EditInPlaceField.prototype = {
createElement : function(id){
this.containerElement = document.createElement("div");
this.parentElement.appendChild(this.containerElement);
this.staticElement = document.createElement("span");
this.containerElement.appendChild(this.containerElement);
this.static.innerHTML = this.value;
this.fieldElement = document.createElement("input");
this.fieldElement.type = "text";
this.fieldElement.value = this.value;
this.containerElement.appendChild(this.fieldElement);
this.saveBtn = document.createElement("input");
this.saveBtn.type = "button";
this.saveBtn.value = "Save";
this.containerElement.appendChild(this.saveBtn);
this.cancelBtn = document.createElement("input");
this.cancelBtn.type = "button";
this.cancelBtn.value = "Cancel";
this.containerElement.appendChild(this.cancelBtn);
this.convertToText();
},
attachEvents : function(){
var me = this;
addEvent(this.staticElement, "click", function(){
me.convertToEditable();
});
addEvent(this.saveBtn, "click", function(){
me.save();
});
addEvent(this.cancelBtn, "click", function(){
me.cancel();
});
},
convertToEditable : function(){
this.staticElement.style.display = "none";
this.fieldElement.style.display = "inline";
this.saveBtn.style.display = "inline";
this.cancelBtn.style.display = "inline";
this.setValue(this.value);
},
save : function(){
this.value = this.getValue;
var me = this;
var callback = {
success : function(){
me.convertToText();
},
failure : function(){
alert("Error saving value.");
}
};
ajaxRequest("GET", "save.php?id=" + this.id + "&value=" + this.value, callback);
},
cancel : function(){
this.convertToText();
},
converToText : function(){
this.fieldElement.style.display = "none";
this.saveBtn.style.display = "none";
this.cancelBtn.style.display = "none";
this.staticElement.style.display = "inline";
this.setValue(this.value);
},
setValue : function(){
this.fieldElement.value = value;
this.staticElement.innerHTML = value;
},
getValue : function(){
return this.fieldElement.value;
}
};
要创建一个就地编辑域,只需实例化这个类即可。
var titleClassical = new EditInPlaceField("titleClassical", $("doc"), "Title Here");
var currentTitleText = titleClassical.getValue();