JavaScript面向对象(4)——最佳继承模式(从深拷贝、多重继承、构造器借用,到组合寄生式继承)

       很多同学甚至在相当长的时间里,都忽略了JavaScript也可以进行面向对象编程这个事实。一方面是因为,在入门阶段我们所实现的各种页面交互功能,都非常顺理成章地使用过程式程序设计解决了,我们只需要写一些方法,然后将事件绑定在页面中的DOM节点上便可以完成。尤其像我这类一开始C++这类语言没好好学,第一门主力语言就是JavaScript的同学来说,过程化程序设计的思维似乎更加根深蒂固。另一方面,就算是对于Java、C++等语言的程序员来说,JavaScript的面向对象也是一个异类:JavaScript中没有class的概念(在ES5及之前版本中没有,ES6会单独介绍),其基于prototype的继承模式也与传统面向对象语言不同,而JavaScript的弱类型特性更会令这里面的很多人抓狂。当然,在熟悉了之后,这种灵活性也会带来很多好处。总之,封装、继承、多态、聚合这些面向对象的基本特性JavaScript都有其自己的实现方式,这些知识的学习是从入门级JS程序员进阶的必经之路。

JavaScript面向对象(1)——谈谈对象

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继承的实现模式了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值