JS中的实现继承主要是依靠原型链来实现的。(与实现继承相对应的还有接口继承)
先说说原型链:
即让原型对象等于另一个类型的实例,即重写原型对象,代之以一个新类型的实例,像下面这样:
function Super(){
this.supProperty = true;
}
Super.prototype.getSuperValue = function(){
return this.supProperty
}
function Sub(){
this.subProperty = false;
}
Sub.prototype = new Super();// 继承
Sub.prototype.getSubValue = function(){
return this.subProperty;
}
var sub = new Sub();
alert(sub.getSuperValue()); // true
alert(sub.getSubValue()); // false
另外提一点,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因
子类型有时候需要覆盖父类型中的某个方法,或者需要添加父类中不存在的方法,给原型添加新方法的代码一定要放在替换原型的语句之后,像上面的Sub.prototype.getSubValue那样,放在替换原型语句之后。
另外,在通过原型链实现继承时,不能使用对象字面量创建原型方法,前面已经提到过,这样会切断原型链。
原型链的问题:
- 前面已经提到过,当原型中包含引用类型值时,会被所有实例共享,所以应该要在构造函数中定义属性而非原型对象中定义属性,回顾一下:
function Super(){
this.colors = ["red","green","pink"];
}
function Sub(){}
Sub.prototype = new Super();
var sub1 = new Sub();
sub1.colors.push("black");
alert(sub1.colors); // red,green,pink,black
var sub2 = new Sub();
alert(sub2.colors); // red,green,pink,black
说明一下,这里的colors属性是在父类构造函数中定义的,当通过原型链被子类继承后,即相当于是在子类的原型对象中定义了colors属性,所以会被所有子类实例所共享,这是不应该的,我们可以借用构造函数来解决这个问题:它的思想是在子类型构造函数内部调用父类型构造函数,而函数只不过是在待定环境中执行代码的对象,所以可以通过使用apply(),call()方法可以在新创建的对象上执行构造函数。像下面这样:
function Super(){
this.colors = ["red","green","pink"];
}
function Sub(){
Super.call(this);//这里的this,代表Sub
}
Sub.prototype = new Super();
var sub1 = new Sub();
sub1.colors.push("black");
alert(sub1.colors); // red,green,pink,black
var sub2 = new Sub();
alert(sub2.colors); // red,green,pink
使用call()方法,实际上是在将要创建的Sub实例的环境下调用了Super构造函数,这样一来,就会在新的Sub对象上执行Super()函数中定义的所有对象初始化代码。结果就是每个Sub的实例都会自己的colors属性了。
- 传递参数
通过借用构造函数,还解决了在子类型构造函数中向父类型构造函数传参数的问题,像下面这样:
function Super(name){
this.name = name;
}
function Sub(age){
Super.call(this,"Stan");//这里的this,代表Sub
this.age = age;
}
Sub.prototype = new Super();
var sub = new Sub(0000);
alert(sub.name); //继承来的 Stan
alert(sub.age); //实例属性 0
单独使用构造函数,不使用原型链话,一是方法都在构造函数中定义,因此函数复用就无从谈起了,而且在父类型的原型中定义的方法,在子类型中是不可见的。所以就有了下面的组合继承
组合继承
这种模式是目前用最多的,先看下面的代码:
function Super(name){
this.name = name;
this.colors = ["red","green","pink"];
}
Super.prototype.sayName = function(){
alert(this.name);
}
function Sub(name,age){
this.age = age;
Super.call(this,name); // 这里不要忘记传参数name
}
Sub.prototype = new Super();
// 这里如果传参数会怎么样? new Super("haha")其实是不会影响的,会被传进来的属性屏蔽掉
Sub.prototype.sayAge = function(){
alert(this.age);
}
var sub1 = new Sub("Stan",11);
sub1.sayName(); // Stan
sub1.sayAge(); // 11
sub1.colors.push("black");
alert(sub1.colors); // red,green,pink,black
var sub2 = new Sub("Joe",22);
alert(sub2.colors); // red,green,pink
以上是借用构造函数和原型链一起做了优化,解决了引用类型属性的问题。这是目前最常用的模式,但它不是最完美的,分析一下:
function Super(name){
this.name = name;
this.colors = ["red","green","pink"];
}
Super.prototype.sayName = function(){
alert(this.name);
};
function Sub(name,age){
this.age = age;
Super.call(this.name); // 第二次调用 Super()构造函数
}
Sub.prototype = new Super(); // 第一次调用Super()构造函数
Sub.prototype.sayAge = function(){
alert(this.age);
}
所以可以看到,上面是两次调用了Super()构造函数,其实这样是没有必要的,我们所需要的无非是父类中的所有属性,即父类型原型的一个副本而已,所以寄生组合式继承就可以解决这个问题,看下面的代码 :
function create(obj){
function fun(){}; // 先定义一个函数
fun.prototype =obj;
return new fun();
}
function inheritPrototype(sup,sub){
sub.prototype = create(sup.prototype);
}
function Super(name){
this.name = name;
this.colors = ["red","green","pink"];
}
Super.prototype.sayName = function(){
alert(this.name);
};
function Sub(name,age){
this.age = age;
Super.call(this,name); // 只调用一次Super()构造函数
}
inheritPrototype(Super,Sub)
Sub.prototype.sayAge = function(){
alert(this.age);
}
var sub = new Sub("Stan",11);
sub.sayName(); // Stan
sub.sayAge(); // 11
说明一下,这个模式的高效率体现在它只调用了一次Super()构造函数,并且因此避免了在Sub.prototype上面创建不必要的、多余的属性,同时,原型链还能保持不变,所以这种模式普遍认为是引用类型最理想的继承范式。