欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-01-03 11:27:28
...

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

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

JavaScript面向对象(2)——谈谈函数(函数、对象、闭包)

JavaScript面向对象(3)——原型与基于构造函数的继承模式(原型链)

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

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对象的结构

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

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

        从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面向对象(4)——最佳继承模式(从深拷贝、多重继承、构造器借用,到组合寄生式继承)

        这便是目前公认最佳的JavaScript继承的实现模式了。

上一篇:

下一篇: