上一篇介绍了创建对象的5种模式,本篇介绍对象实现继承的3种形式。继承简单说就是在原有对象基础上稍作改动,得到一个新的对象,这个新对象可以拥有原对象的属性和方法。JS实现继承的3种方式:类式继承、class继承和拷贝继承。
JS这门语言和其他面向对象的语言不同,它并不支持类和类继承特性,只能通过其他方法定义并关联多个相似对象。虽然ES6增加了class关键字,但只是构造函数的语法糖而已,并不是意味着JS中是有类的。
类式继承
类式继承本质是通过构造函数实例化对象,然后用原型链把实例对象关联起来。
原型链继承
原型链继承本质是通过超类型的实例重写子类型的原型对象。
// 超类
function Super(){
this.name = 'li'
}
Super.prototype.sayName = function(){
return this.name
}
// 子类
function Sub(){}
// Sub继承Super
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
var s1 = new Sub()
console.log(s1.sayName()) // 'li'
原型链继承有两个问题:第一是如果包含引用类型值会被所有实例共享;第二是在创建子类型实例时不能向超类型传递参数,因为会影响所有对象实例。
function Super() {
this.hobbies=['编程']
}
Super.prototype.sayHobby=function() {
return this.hobbies
}
function Sub() {}
Sub.prototype=new Super()
Sub.prototype.constructor=Sub;
var s1=new Sub();
var s2=new Sub();
s1.hobbies.push('音乐')
console.log(s1.sayHobby()) // ['编程','音乐']
console.log(s2.sayHobby()) // ['编程','音乐']
借用构造函数继承
借用构造函数继承(constructor stealing)也叫伪类继承或经典继承。它的思想是在子类型构造函数内部调用超类型构造函数,并通过apply()或call()方法使子类型实例继承超类型的属性和方法。
function Super() {
this.hobbies=['编程']
}
function Sub() {
Super.call(this)
}
var s1=new Sub();
var s2=new Sub();
s1.hobbies.push('音乐')
console.log(s1.hobbies) // ['编程','音乐']
console.log(s2.hobbies) // ['编程']
使用这种方式不仅可以解决引用类型值共享的问题,而且可以在子类型构造函数中向超类型构造函数传递参数。
function Super(hobbies) {
this.hobbies=hobbies
}
function Sub() {
Super.call(this,['编程'])
this.school='Tsinghua'
}
var s1=new Sub();
console.log(s1.hobbies) // ['编程']
console.log(s1.school) // 'Tsinghua'
但是使用这种方式,方法和属性都要在构造函数中定义,导致函数无法复用。
组合继承
组合继承(combination inheritance)也叫伪经典继承。它是原型链继承和借用构造函数继承的组合,取两者的优势。组合继承的思路是使用原型链实现对原型属性和方法的继承,使用构造函数实现对超类型实例属性的继承。这样不仅实现了函数的复用,而且保证了每个实例都有自己的属性。
function Super(name) {
this.name=name;
this.school='Tsinghua';
this.hobbies=['编程','音乐'];
}
Super.prototype.sayName=function() {
return this.name;
}
function Sub(name, age) {
// 继承超类属性
Super.call(this, name);
// 定义子类属性
this.age=age;
}
// 继承超类方法
Sub.prototype=new Super();
Sub.prototype.constructor=Sub;
// 定义子类方法
Sub.prototype.sayAge=function() {
return this.age;
}
var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);
console.log(p1.school, p1.sayName(), p1.sayAge()) // 'Tsinghua' 'li' 10
console.log(p2.school, p2.sayName(), p2.sayAge()) // 'Tsinghua' 'wang' 20
p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"] ["编程", "音乐"]
组合模式似乎很完美,但是它也有一个问题,那就是它会调用两次超类型的构造函数。一次是在子类构造函数内部,一次是在创建子类型原型的时候。每次调用子类型都会得到超类型的属性,但得不到超类型的方法,只有两次调用配合才能继承超类型的方法。
function Super(name) {
this.name=name;
this.school='Tsinghua';
this.hobbies=['编程','音乐'];
}
Super.prototype.sayName=function() {
return this.name;
}
function Sub(name, age) {
// 第二次调用 Sub.prototype又得到了name、school和hobbies属性,并覆盖上次得到的值
Super.call(this, name);
// 定义子类属性
this.age=age;
}
// 第一次调用 Sub.prototype得到了name、school和hobbies属性
Sub.prototype=new Super();
Sub.prototype.constructor=Sub;
Sub.prototype.sayAge=function() {
return this.age;
}
寄生组合继承
寄生组合继承解决了组合继承调用两次的问题,寄生组合继承和组合继承类似,它是寄生式继承和构造函数继承的组合。它的思路是通过构造函数实现对实例属性的继承,通过Object.create()方法实现对原型属性和方法的继承。
function Super(name) {
this.name=name;
this.school='Tsinghua';
this.hobbies=['编程', '音乐'];
}
Super.prototype.sayName=function() {
return this.name;
}
function Sub(name, age) {
Super.call(this, name);
this.age=age;
}
Sub.prototype=Object.create(Super.prototype);
Sub.prototype.constructor=Sub;
Sub.prototype.sayAge=function() {
return this.age;
}
寄生组合继承是认可度最高的继承方式。
ES6 class
第二种继承方式是使用ES6中的class,它思路上和类式继承一样,只不过隐藏了很多类式继承的细节。如果使用ES6的class改写上面的示例,代码会精简很多。
class Super {
constructor(name) {
this.name = name;
this.school = 'Tsinghua';
this.hobbies = ['编程','音乐'];
}
sayName() {
return this.name;
}
}
class Sub extends Super {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
return this.age
}
}
var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);
console.log(p1.school, p1.sayName(), p1.sayAge()) // 'Tsinghua' 'li' 10
console.log(p2.school, p2.sayName(), p2.sayAge()) // 'Tsinghua' 'wang' 20
p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"] ["编程", "音乐"]
拷贝继承
拷贝继承不需要改变原型链,它的思路是通过对象深拷贝将超类型的属性和方法复制给子类型。拷贝继承解决了引用类型值被实例共享的问题,所以可以不适用构造函数实现对象的继承。JQuery使用的就是拷贝继承。
关于深拷贝和浅拷贝移步此文
// 深拷贝函数
function extend(obj, cloneObj) {
var isObj = obj instanceof Object;
if(!isObj) {return false;}
var cloneObj = cloneObj || {};
for (var i in obj) {
if (typeof obj[i] === 'object') {
cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
arguments.callee(obj[i], cloneObj[i]);
} else {
cloneObj[i] = obj[i];
}
}
return cloneObj;
}
var Super = {
init: function(value){
this.value = value
},
sayName: function(){
return this.name
},
hobbies: ['编程'],
school: 'Tsinghua'
}
var s1 = extend(Super);
var s2 = extend(Super);
s1.hobbies.push('音乐');
console.log(s1.hobbies,s2.hobbies) // ["编程", "音乐"] ["编程"]
构造函数的拷贝组合继承
如果构造函数和拷贝继承组合使用,则可以拷贝继承超类型的引用类型值和方法,利用构造函数定义子类型的属性值。
// 深拷贝函数
function extend(obj, cloneObj) {
var isObj = obj instanceof Object;
if(!isObj) {return false;}
var cloneObj = cloneObj || {};
for (var i in obj) {
if (typeof obj[i] === 'object') {
cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
arguments.callee(obj[i], cloneObj[i]);
} else {
cloneObj[i] = obj[i];
}
}
return cloneObj;
}
function Super(name){
this.name = name;
this.school = 'Tsinghua';
this.hobbies = ['编程','音乐'];
}
Super.prototype.sayName = function() {
return this.name
}
function Sub(name,age){
Super.call(this,name);
this.age = age;
}
Sub.prototype = extend(Super.prototype);
var p1=new Sub('li', 10);
var p2=new Sub('wang', 20);
console.log(p1.school, p1.sayName()) // 'Tsinghua' 'li'
console.log(p2.school, p2.sayName()) // 'Tsinghua' 'wang'
p1.hobbies.push('跑步');
console.log(p1.hobbies, p2.hobbies); // ["编程", "音乐", "跑步"] ["编程", "音乐"]