说到Javascript的继承,总结大家对“继承”的实现方法,可能已经达到十多种了(《Javascript面向对象编程指南》列举出了12种方法),但如果真正理解了继承的本质,便会发现真正意义上的继承,可能就只剩没两个了,故,在前面对继承打了引号。
一、什么是继承?
先来看看《Javascript王者归来》中对继承的定义:
前面已经说过,如果两个类都是同一个实例的类型,那么它们之间存在着某些关系,我们把同一个实例的类型之间的泛化关系称为“继承”。
这很容易理解,例如,“白马”是一种“马”,“白马”和“马”之间的关系就是继承关系。“一匹白马”是“白马”的一个实例,“白马”是“一匹马”的类型,而“马”同样是“一匹马”的类型,“马”是“白马”的泛化,所以“白马”继承自“马”。
一旦确定了两个类的继承关系,就至少意味着三层含义,
一是,子类的额实例可以共享父类的方法,
二是,子类可以覆盖父类的方法或者扩展新的方法,
三是,子类和父类都是子类实例的“类型”。
在Javascript中,并不直接从文法上支持继承,换句话说,Javascript没有实现“继承”的语法,从这个意义上来说,Javascript并不是直接的面向对象的语言。
在Javascript中,继承是通过模拟的方法来实现的。
上述对继承的定义,很好诠释了继承的本质,只有
深刻理解这三层含义,才能透彻理解继承。
二、继承的实现层面
和众多的图书中对继承方法的汇总一样,很多人都只是对N种实现方法的堆砌,所以让人阅读起来感觉很混乱,看时懂了,看完迷糊。所以在列举方法之前,需要先区分这些方法的实现层面。继承的实现层面有如下两种:
一、构造函数层面的实现方法(比如组合继承)
二、对象层面的实现方法(比如深浅拷贝)
后面会分析各种“继承”方式的优缺点,最后会总结出哪些才是实现了真正意义上的继承。
三、JavaScript中的值类型和引用类型(可参考文章
JavaScript值类型和引用类型)
看到这儿你可能会问,写继承怎么说到这两个概念去了,跑题了啊!但提醒你的是,这两个概念会在后面对某些继承方式缺点的描述中多次提到,所以也
必须透彻理解。
1、JavaScript值类型和引用类型有哪些
(1)值类型:数值、布尔值、null、undefined。
(2)引用类型:对象、数组、函数。
2、如何理解值类型和引用类型及举例
(1)值类型理解:变量的交换等于在一个新的地方按照连锁店的规范标准(统一店面理解为相同的变量内容)新开一个分店,这样新开的店与其它旧店互不相关、各自运营。
(2)引用类型理解:变量的交换等于把现有一间店的钥匙(变量引用地址)复制一把给了另外一个老板,此时两个老板同时管理一间店,两个老板的行为都有可能对一间店的运营造成影响。
四、继承的实现方式汇总
构造函数层面
1、原型链继承
2、借用构造函数(伪造对象/经典继承)
3、组合继承(伪经典继承/类抄写)
对象层面
4、原型式继承
5、寄生式继承
6、寄生组合式继承
7、实例继承
五、各继承方式分析
1、原型链继承(关于prototype可参考上一篇【读书笔记】之Javascript prototype(JS原型)深入理解)
实现方式很简单:
function SuperType() {
this.flag = true;
}
SuperType.prototype.getSuperFlag = function() {
return this.flag;
};
function SubType() {
this.subFlag = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
//扩展了自己的方法
SubType.prototype.getSubFlag = function() {
return this.subFlag;
};
var st = new SubType();
原型链继承的优点:强大、实现起来方便快捷
当然
缺点也是明显的:
第一个问题是,包含引用类型值的原型属性会被所有实例共享,改变一个实例的引用值类型属性,会直接影响到其它实例。而这也正是为什么要在构造函数中,而不是原型中定义属性的原因。
看看下图就知道了:
你发现了什么没有?
当改变子类的实例st1“自己的”colors数组属性时,其它的实例的colors也变成一样的了,其实这个colors数组属性并不是真正属于自己,而是对 SubType原型中该数组(SubType.prototype.colors)的引用(C语言中的指针了解吧,st1.colors -> SubType.prototype.colors <- st2.colors),子类的实例的colors属性都引用于这个数组, 所以修改后直接影响到其它所有实例的该属性。修改name属性也是同样的道理。
第二个问题是:在创建子类型的实例时,不能通过new SubType(args)的方式向超类型的构造函数中传递参数。
看看下图:
第三个问题是:无法直接实现多重继承(比如继承自两个或三个超类)
2、借用构造函数(伪造对象/经典继承)
正是由于原型链继承方式中,对引用值类型的数据处理的缺点,所以才出现了这个借用构造函数的继承方式。
这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。 别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用applay()和call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示:
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
SuperType.call(this); //调用SuperType()构造函数,并将当前this传入
}
var st1 = new SubType();
st1.colors.push('black');
alert(st1.colors); //'red,blue,green,black'
var st2 = new SubType();
alert(st2.colors); //'red,blue,green'
看看下图:
子类的实例st1和st2的colors属性互不干扰了。这到底是为嘛呢?
SubType实例化时,比如st1,在SubType中this指向这个st1,SuperType.call(this)相当于,st1.colors=['red', 'blue', 'green'];同理,st2也是如此,所以st1和st2各自有了真正属于自己的colors属性而不是引用自原型中的。
借用构造函数的优点:
一是,解决了原型链继承中共享原型引用值类型属性的问题;
二是,相对于原型链而言,有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数
看下图:
缺点也很明显:
并没有实现共享,SubType中的方法根本得不到复用,而且,在超类型的原型中定义的方法,对子类型而言也是不可见的。(见图片中超类原型中的name属性) 结果所有类型都只能使用构造函数模式。 考虑到这些问题,借用构造函数技术也是很少单独使用的。
(关于第二个优点这个地方为什么没有引用《Javascript高级程序设计》中的例子,个人觉得《高级程序设计》中这个地方完全没有把这个优点讲解明白,相反让人很疑惑很费解)
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);
this.age = age;
}
SubType.prototype = new SuperType();
var st1 = new SubType('大毛',23);
st1.colors.push('black');
var st2 = new SubType('二毛',22);
看下图:
可以看到组合继承优势很明显了,规避了前两种方法(原型链继承、借用构造函数)的缺点,整合了前两种方法的优势。不再共享引用值类型,所以各自不会干扰,同时共享了超类原型中的方法。个人觉得这个才应该是经典。
4、原型式继承
前面谈到的几种继承方式都是构造函数层面实现的继承,下面集中属于对象层面的继承了
原型式继承,这种方法并没有使用严格意义上的构造函数。 它借助原型可以基于已有对象创建新对象,同时还不必因此创建自定义类型。 为了达到这个目的,给出如下函数:
function extend(o) {
function F () {}
F.prototype = o;
return new F();
}
在extend()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
从本质上讲,object()对传入其中的对象执行了一次浅复制。
未完待续,明日再续~