JavaScript面向对象(4)——最佳继承模式(从深拷贝、多重继承、构造器借用,到组合寄生式继承)
很多同学甚至在相当长的时间里,都忽略了JavaScript也可以进行面向对象编程这个事实。一方面是因为,在入门阶段我们所实现的各种页面交互功能,都非常顺理成章地使用过程式程序设计解决了,我们只需要写一些方法,然后将事件绑定在页面中的DOM节点上便可以完成。尤其像我这类一开始C++这类语言没好好学,第一门主力语言就是JavaScript的同学来说,过程化程序设计的思维似乎更加根深蒂固。另一方面,就算是对于Java、C++等语言的程序员来说,JavaScript的面向对象也是一个异类:JavaScript中没有class的概念(在ES5及之前版本中没有,ES6会单独介绍),其基于prototype的继承模式也与传统面向对象语言不同,而JavaScript的弱类型特性更会令这里面的很多人抓狂。当然,在熟悉了之后,这种灵活性也会带来很多好处。总之,封装、继承、多态、聚合这些面向对象的基本特性JavaScript都有其自己的实现方式,这些知识的学习是从入门级JS程序员进阶的必经之路。
JavaScript面向对象(2)——谈谈函数(函数、对象、闭包)
JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链)
JavaScript面向对象(4)——最佳继承模式(深拷贝、多重继承、构造器借用、组合寄生式继承)
一、基于对象工作模式的继承
我们知道,JavaScript中创建对象主要有两种方式:构造函数与对象直接量。上一篇中介绍的三种继承方法也都是基于构造函数进行工作的,这种方法更类似与Java式的继承方式,构造函数和原型对象就相当于Java中的类了。 然而,JavaScript中终究是没有类的概念的,一切的核心还是对象。下面介绍的就是这类方法:
1、浅拷贝
//浅拷贝
function extend(p){
var obj = {};
for(var i in p) obj[i] = p[i];
obj.father = p;
return obj;
}
var fatherObj = {
name: 'father',
toString: function(){return this.name;}
}
var a = extend(fatherObj);
a.name = 'aaa';
a.toString(); // 'aaa'
这个继承函数的唯一参数是父对象(注意这里接受的是父对象,也就是父类的实例对象。上一节的拷贝法中接受的是父类的构造函数对象),将父对象的全部属性拷贝至子对象中,并在子对象中添加father属性以方便引用父对象。当然了,由于是直接拷贝,父对象中值为对象的属性依然是以引用的方式拷贝的,在子对象中修改此类属性会影响到父对象。 下面是这种方法得到的a对象的结构
2、深拷贝
//深拷贝
function deepCopy(p, c){
var c = c || {};
for( var i in p){
if(typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
}else if(typeof p[i] === 'function'){
c[i] = p[i].prototype.constructor;
}else c[i] = p[i];
}
return c;
}
var fatherObj = {
name: 'father',
hobby: ['football','basketball'],
toString: function(){return this.hobby}
}
//测试
var a = deepCopy(fatherObj);
console.log(a.toString()); // ['football','basketball']
console.log(a.hobby === fatherObj.hobby); //false
console.log(a.toString === fatherObj.toString); //false
相对于之前的浅拷贝,深拷贝则是对于对象做了特殊的处理:在遍历父对象属性是,一旦发现该对象为对象属性,递归调用自身将该对象进行复制。另外,由于函数对象无法直接通过属性遍历的方法进行深拷贝,这里通过访问方法对象的原型对象的constructor属性并将其进行赋值这个小技巧,完成了属性的深拷贝。这个方法由于在处理对象深拷贝时需要递归调用,没有在方法内添加父对象的引用,在使用的时候可以手动进行添加或者对这个方法进行二次封装。
拷贝与深拷贝其实也是聚合的实现了,将其他对象的属性拿过来扩展自身对象。若是两对象为父级子级关系,则为继承;若是两对象同级扩展,则可以视作聚合。其核心点就是深拷贝。
3、通过直接设置原型对象进行继承
//直接设置原型对象
function extend(p) {
function F(){};
F.prototype = p;
var c = new F();
c.father = p;
return c;
}
这个方法接受父对象为唯一参数,并将父对象设置为临时构造器的原型对象,构造出子对象,完成继承。Object对象中包含了create方法,功能与这个大概一致,都是接受一个对象作为参数,返回以该对象为原型对象的新对象,MDN中有详细的解释:MDN:Object.create()4、多重继承
显然,JavaScript不可能为多重继承提供语法单元。但是对于JavaScript这类语言来说,模拟出多重继承也是非常容易的。这里提供了一种基于对象拷贝的多重继承实现:
//多重继承
function multiple(){
var c = {},
stuff,
len = arguments.length;
c['father'] = [];
for(var j = 0;j < len;j++){
stuff = arguments[j];
for(var i in stuff) c[i] = stuff[i];
c['father'].push(stuff);
}
return c;
}
JavaScript中实参的个数可以多于形参,利用这个特性我们可以方便的处理任意数量个参数。这里的方法就可以从任意个对象中继承属性,将这些属性拷贝至新对象中,并将父对象的引用添加值father属性中,将构造完成的子对象返回。 同样的,可以轻松地将这个方法改写成现有对象之间的继承:
//多重继承2
function multiple(/*第一个参数为子对象,其余为父对象*/){
var c = arguments[0],
stuff,
len = arguments.length;
c['father'] = [];
for(var j = 1;j < len;j++){
stuff = arguments[j];
for(var i in stuff) c[i] = stuff[i];
c['father'].push(stuff);
}
return c;
}
当然了,若遇到同名属性,会按照先后次序覆盖。二、构造函数借用
还有一类很重要的继承实现方式,称为构造器借用(构造函数借用)。这里是利用了call()或apply()方法在子对象构造函数中调用父对象的构造函数。
//构造器借用
function Animal(age){
this.age = age;
}
Animal.prototype.getAge = function(){ return this.age + ' years old.'};
function Bird(){
Animal.apply(this, arguments);
}
Bird.prototype.className = 'Bird';
var a = new Bird(10);
console.log( a.className ); // 'Bird'
console.log( a.age); // 10
console.log( a.getAge ); // undefined
在这种继承模式中,子对象不会继承父对象的原型属性,只会将父对象在构造函数中定义的属性重建在自身属性中。并且遇到值为对象的属性时,也会获得一个新值,而不是父级该值的引用。同时,对子对象所做的任何修改都不会影响父对象。
三、找出最佳的继承方法
1、寄生式继承
这个方法其实是对原型对象法的升级,将继承后对象的扩展也封装进方法中。“这样在创建对象的函数中直接吸收其他对象的功能,进行扩展并返回,好像所有工作都是自己做的”,便是寄生式继承名字的由来了。这里直接使用了Object.create()方法,也可以用上文中给出的方法。
//寄生式继承
function extend(p){
var c = Object.create(p);
//在此对c进行扩展,添加子对象的自有属性和方法
//......
//......
return c;
}
2、组合继承
这个方法是构造器借用法的延伸。由于构造器借用法无法继承原型属性,无法实现函数复用。便在该方法上做了简单改动: BIrd.prototype = new Animal()
//组合继承
function Animal(age){
this.age = age;
}
Animal.prototype.getAge = function(){ return this.age + ' years old.'};
function Bird(){
Animal.apply(this, arguments);
}
Bird.prototype.className = 'Bird';
var a = new Bird(10);
console.log( a.className ); // 'Bird'
console.log( a.age ); // 10
console.log( a.getAge ); // '10 years old.'
然而,这种方式也有个明显的缺点。在继承的过程中,父对象的构造函数会被调用两次:apply方法会调用一次,随后调用子对象构造函数时又会调用一次。父对象的自身属性实际上被继承了两次:
function Animal(age){
this.age = age;
}
function Bird(){
Animal.apply(this, arguments);
}
Bird.prototype = new Animal(100)
Bird.prototype.className = 'Bird';
var a = new Bird(200);
console.log(a.age); // 200
console.log(a.__proto__.age); // 100
delete a.age;
console.log(a.age); // 100
从a对象的结构中可以清晰的看出,其自身重建了父级属性age,又从原型中继承了age,该属性被继承了两次。
将继承原型的方式从本例中的方法替换为 JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链) 中的最后一种方法可以更正双重继承的问题。然而由于该方法本身的问题与局限性,这还不是最佳的方案。
3、最佳继承方法: 组合寄生式继承
说了这么多,终于该引出最佳方法了:组合寄生式继承法。这里直接搬出红宝书里的经典源码:
function inherit(subType, superType){
var protoType = Object.create(superType.prototype);
protoType.constructor = subType;
subType.prototype = protoType;
}
该方法接受子类和父类构造函数作为参数,构造出子类构造函数的原型对象,完成原型的继承,再配合组合式继承法的其余部分:
function inherit(subType, superType){
var protoType = Object.create(superType.prototype);
protoType.constructor = subType;
subType.prototype = protoType;
}
function Animal(age){
this.age = age;
}
Animal.prototype.getAge = function (){ return '11'};
function Bird(){
Animal.apply(this, arguments);
}
inherit(Bird, Animal);
Bird.prototype.className = 'Bird';
Bird.prototype.getName = function(){ return '22'};
这便是目前公认最佳的JavaScript继承的实现模式了。