JavaScript实现继承的方式和原理
说明:参考了很多帖子,以这个为蓝本:http://www.jb51.net/article/81766.htm
对于从C++或Java等转来的程序员,理解JavaScript的“类-继承”思想可能不是显而易见的。简而言之,JavaScript实现的是实现继承而非接口继承,主要依靠原型链来实现。实现继承的方式也有多种。
0、构造函数、原型、实例之间的关系
每个构造函数(constructor)都有一个原型对象(prototype);
原型对象(prototype)包含一个指向构造函数的指针(constructor)
实例(instance)都包含一个指向原型对象(prototype)的内部指针。
1、原型链模式(默认)
基本思想:利用原型(prototype)让一个引用类型继承另一个引用类型的属性和方法。
原型链实现继承的例子:
// SuperType就是一个构造函数(constructor)
function SuperType() {
this.property = true;
}
// 构造函数SuperType有一个原型对象(prototype)
SuperType.prototype.getSuperValue = function() {
return this.property;
}
// SubType是“子类”构造函数(constructor)
function SubType() {
this.property = false;
}
// !!!将构造函数SubType的原型对象(prototype)指向SuperType的一个实例
SubType.prototype = new SuperType(); //SuperType实例包含指向prototype的指针
SubType.prototype.getSubValue = function () {
return this.property;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //实例instance实际上指向SubType.prototype
分析:
生成SuperType实例时,会自动生成一个prototype。利用该prototype,可以定义“类”的函数(SuperType.prototype.getSuperValue)。
“子类”SubType构造时也生成一个prototype,但紧接着就将SubType.prototype指向SuperType的一个实例。而从使用的角度来看,对实例(instance)的引用就是对实例中prototype指针的引用:比如instance.getSuperValue()实际上就是对SuperType.prototype.getSuperValue的调用。
SubType.prototype=new SuperType();中,将SubType.prototype指向了SuperType的一个实例——进而指向SuperType的prototype。这样,SubType就继承了SuperType期中的所有内容。接下来定义SubType.prototype.getSubValue,就是在SubType.prototype中添加了更多内容。
所以这其中的关键是prototype机制,需要详细研究一下。
2、借用构造函数
基本思想:在子类型构造函数的内部调用超类构造函数,使用call()和apply()在新建对象上执行构造函数。
function SuperType() {
this.colors = ["red","blue","green"];
}
// 子类型构造函数
function SubType() {
SuperType.call(this); //用call调用SuperType的构造函数
// this作为参数:用子类型的this替换SuperType的this
// 使SuperType的构造函数作用在SubType的实例上
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red","blue","green","black"
var instance2 = new SubType();
console.log(instance2.colors); //"red","blue","green"
call()和apply()也可以用于别的函数。
这里,SuperType是写死(hardcode)的,不灵活,实际上可以利用自定义的parent的constructor来替代。
3、组合继承
基本思想:将原型链和借用构造函数技术组合起来,发挥两者之长。
function SuperType(name) { this.name = name; this.colors = ["red","blue","green"]; }
SuperType.prototype.sayName = function() { console.log(this.name); }
function SubType(name, age) {
SuperType.call(this,name); // “父类”构造函数,name作为SuperType参数
this.age = age; // “子类”自己的构造内容
}
SubType.prototype = new SuperType(); // 继承SuperType的prototype
// !-将子类prototype中的构造函数constructor指针指向构造函数SubType
// 在第一种方法中,由于将SubType.prototype指向了SuperType.prototype,
// 所以SubType.prototype.constructor就指向了SuperType.prototype.constructor,
// 也就是SuperType()
// 但是,经过测试发现:是否有下面这一行,结果都一样,还不理解!
Subtype.prototype.constructor = SubType;
Subtype.prototype.sayAge = function() { console.log(this.age); }
// 生成SubType的实例
var instance1 = new SubType("EvanChen",18);
instance1.colors.push("black");
consol.log(instance1.colors); //"red","blue","green","black"
instance1.sayName(); //"EvanChen"
instance1.sayAge(); //18
var instance2 = new SubType("EvanChen666",20);
console.log(instance2.colors); //"red","blue","green"
instance2.sayName(); //"EvanChen666"
instance2.sayAge(); //20
4、原型继承模式(有待研究)
基本想法:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义的类型。原型式继承的思想可用以下函数来说明:
function object(o) { // object是一个“壳”构造函数,o是父类型
function F(){} // F是一个空的构造函数(临时的)
F.prototype = o; // 将F的prototype指向期望的“父类”的实例(?)o
return new F(); // 返回F的实例(也就是object的实例)
}
例子:
var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby","Court","Van","Rob","Barbie"
ECMAScript5通过新增Object.create()方法规范化了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和一个作为新对象定义额外属性的对象。
var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby","Court","Van","Rob","Barbie"
5、寄生式继承(有待研究)
基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正是它做了所有工作一样返回对象。
function createAnother(original) { // 函数createAnother仅用于封装继承过程
var clone = object(original); // ?
clone.sayHi = function () { // 增强对象
alert("hi");
};
return clone; // 返回增强对象
}
var person = {
name:"EvanChen",
friends:["Shelby","Court","Van"];
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
6、寄生组合式继承
基本思想:通过借用函数来继承属性,通过原型链的混成形式来继承方法。基本模型如下所示:
function inheritProperty(subType, superType) { // 用函数来继承属性
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 构造函数指向subType
subType.prototype = prototype; // subType的prototype指向superType的原型
}
例子:
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function (){
alert(this.name);
};
function SubType(name,age) {
SuperType.call(this,name); // 继承构造函数
this.age = age;
}
// 将SubType.prototype指向SubType.prototype,
// 同时将SubType.prototype的constructor指向SubType
inheritProperty(SubType,SuperType);
SubType.prototype.sayAge = function() {
alert(this.age);
}
总结,以及如何添加parent来提高继承的灵活性
(1)继承的核心在于prototype,要让子类型的prototype指向父类型实例的prototype
① SubType.prototype = new SupperType();
(2)prototype中有构造函数指针constructor,应该正确设置
① 指向子类型构造函数:SubType.prototype.constructor = SubType;
② 之前SubType.prototype等于SupperType的prototype,构造函数也是SupperType
(3)子类型构造函数中,可借用父类型构造函数
① SupertType.call(this,...),这个是写死的
② 可以在prototype中添加parent,使其指向父类型的prototype
1) SubType.prototype.parent = SupperType.prototype;
2) 可以在子类型构造函数中调用this.parent.constructor.call(this, ...)
3) 比SupertType.call(this,...)优雅一点。不过无伤大雅——类之间继承关系很明确
最终的例子
// 定义父类型
function SuperType(name) { // 构造函数
this.name = name;
}
SuperType.prototype.getNameVal = function () { return this.name; } // 父类型函数
// 定义子类型
function SubType(type, name) { // 构造函数
this.parent.constructor.call(this, name); // 通过parent调用父类型构造函数
this.type = type;
}
SubType.prototype = new SuperType(); // 将子类型的prototype指向父类型实例!!!
// 此处子类型prototype完全等于父类型的
SubType.prototype.constructor = SubType; // 使子类型构造函数指向子类型构造函数
SubType.prototype.parent = SuperType.prototype; // 自定义parent,指向父类型的prototype
SubType.prototype.getType = function () { // 子类型函数
return this.type;
}
SubType.prototype.toString = function() {
return '[SubType "'+ this.objectIndex + '"]';
}
var subInstance = new SubType(‘type1’, ‘name1’);
alert(subInstance.getName());
alert(subInstance.geType());
alert(subInstance.toString());
结果分析
// 父类型中的内容
SuperType = {
this.name: name;
prototype: [getNameVal, toString, ...]
}
// new一个SubType实例: var inst = new SubType(type,name);
// 第一步:
SubType = { prototype: [construct: SubType, toString, ...] }
// 执行this.parent.constructor.call(this, name);继承父类型的属性(方法?)
SubType = { this.name: name; prototype: [construct: SubType, toString, ...] }
(有没有SubType的方法?)
// 执行this.type = type;添加新属性
SubType = { this.name: name; this.type: type; prototype: [construct: SubType, toString, ...] }
// 执行SubType.prototype = new SuperType();
SubType = { this.name: name; this.type: type;
prototype: [construct: SuperType, toString, getNameVal, ...] }
// 执行SubType.prototype.constructor = SubType;
SubType = { this.name: name; this.type: type;
prototype: [construct: SubType, getNameVal, toString, ...] }
// SubType.prototype.parent = SuperType.prototype;
SubType = { this.name: name; this.type: type;
prototype: [construct: SubType, getNameVal, toString,
parent: SuperType.prototype, ...] }
// SubType.prototype.getType = function () { return this.type; }
// SubType.prototype.toString = function() { return '[SubType "'+ this.objectIndex + '"]'; }
SubType = { this.name: name; this.type: type;
prototype: [construct: SubType, getNameVal, toString,
parent: SuperType.prototype, getType, toString, ...] }
有两个toString,这时候子类型的方法会覆盖父类型的方法。