花了几天时间学习了JS中继承的有关知识,总算是有了一个清晰的脉络。下面会附上实例一步步讲解JS中的继承。

一 原型链

JS中很重要的一个概念,并且也是各种面试考试的高频问题。在前面原型的文章中,已经仔细介绍了JS中原型的概念,如果还不太了解,请移步JS中使用原型模式创建对象中进行阅读学习。

原型链是实现继承的主要方法。每个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(__proto__)。假如我们让原型对象A等于另一个类型B的实例,那么原型对象A就包含一个指向另一个类型B的原型对象的指针,以此类推,B的原型对象又是C类型的实例,上面的关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的概念。

function A(){
    this.name = "A";
}
A.prototype.getName = function(){
    console.log(this.name);
}

function B(){
    this.name = "B";
}

B.prototype = new A(); //B的原型对象等于A的实例

B.prototype.getName = function(){
    console.log(this.name);
}

function C(){
    this.name = "C";
}

C.prototype = new B(); //C的原型对象等于B的实例

C.prototype.getName = function(){
    console.log(this.name);
}

上面这种实现的本质是重写了原型对象,将一个新对象A的实例赋值给这个原型对象B,之前存在于实例对象A中的属性和方法现在也存在于B的原型对象中了。所以AB之间建立起了链式的联系。

在以上的代码中,原型链的终端是Object,Object是所有对象的父类。同时注意在给原型上添加方法时应该在继承语句之后。再使用对象字面量添加新方法会导致继承的语句失效。

function A(){
	this.name = "A";
}
A.prototype.getName = function(){
	console.log(this.name);
}

function B(){
	this.name = "B";
}

B.prototype = new A(); //B的原型对象等于A的实例

B.prototype.getName = function(){
	console.log(this.name);
}

function C(){
	this.name = "C";
}

C.prototype = new B(); //C的原型对象等于B的实例

C.prototype = {   //上一句会失效
	getName:function(){
		console.log(this.name);
	}
}

上面代码中刚刚把B的实例赋给原型,紧接着又将原型替换成一个对象字面量,由于对象字面量是一个Object的实例,而非B对象的实例,所以BC之间的原型链被切开。

二 原型链继承存在的问题

上面实例的原型链继承方式存在两个问题。


B的原型对象继承了A实例中的所有属性,包括引用类型friends,因为friends在B的原型上,所以声明的两个实例b1和b2共享该属性,会造成影响。

第二个问题是在创建子类的实例时,不能向父类的构造函数中传递参数。

三 构造函数继承

上面原型链继承存在两个问题,我们用构造函数继承来解决上面的问题。


上面call语句就是把要即将创建好的B对象执行了一遍A中的语句,这样,B就有了A中所有的属性和方法的副本。并且,我们通过call方法给父类传递了参数id。B的构造函数中有了friends属性,所有B的实例b1,b2在实例化的过程中都有自己的friends属性,这样属性就不会共享,当然也就不会互相影响了。

但是上面使用构造函数的方式违背了代码复用的初衷,在父类原型上定义的方法对于子类不可见,要想实现继承,所有的方法都必须放在构造函数中,而每个实例对象b1,b2...都有自己独立的空间存放属性方法。所以我们还要进阶继承方法。

四 组合继承

组合继承结合了原型继承和构造函数继承的各自优点,将属性放在构造函数中使用构造函数继承,方法使用原型继承,来看下面的例子:


这种组合继承的方式解决了原型继承和构造函数继承带来的问题,引用类型在实例间不共享,原型上的方法在子类实例也可以访问,并且子类可以传递构造函数给父类。

但是这种组合继承也存在问题,父类的构造函数调用了两遍。

一次调用是在原型继承的时候给子类的原型赋值父类的实例,第二次是构造函数继承的时候又执行了一遍父类构造函数。这样,子类原型中继承的属性就会被屏蔽。在JS中还有更完美的方式来实现继承,在学习这种方式之前,我们先来学习一个简单并且常用的方式。

五 原型式继承

function inheritObject(o){
    //声明一个过渡函数对象
	function F(){}
        //过渡函数的原型继承父对象
	F.prototype = o;
	//返回过渡函数的实例,该实例的原型继承了父对象
	return new F();
}

这种方式和最前面描述的原型链继承很像,只不过进行了一层封装。也存在原型链继承中共享属性的问题,测试代码如下:


六 原型式继承进阶——寄生式继承

function createAnotherObj(obj){
	var o = new inheritObject(obj);
	o.getOtherProperty = function(){
		console.log(this.property);
	}
	return o;
}

上面就是寄生式继承的模式,实现了对原型式继承的再次封装,生成新对象后,又给新对象添加了自己的属性和方法。这种模式是给下面寄生组合式继承做铺垫。

七 终极继承者——寄生组合式继承

在上面的组合继承中,我们提到用原型链继承和构造函数继承会存在调用两次父类构造函数的问题,那么我们将使用寄生组合式来解决上面的问题,寄生组合式继承的基本模式如下所示:

function inheritObject(o){
    //声明一个过渡函数对象
	function F(){}
	//过渡函数的原型继承父对象
	F.prototype = o;
	//返回过渡函数的实例,该实例的原型继承了父对象
	return new F();
}
function inheritPrototype(subClass,superClass){
    //保存父类原型的副本到变量p中
	var p = inheritObject(superClass.prototype);
	//增强对象
	p.constructor = subClass;
	//设置子类型的原型为父类的原型,实现继承关系
	subClass.prototype = p;
}

组合继承中,通过构造函数继承的属性和方法是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数。我们需要的就是父类原型对象的一个副本,而这个副本我们通过原型继承就可以得到,但是这么直接赋值给子类会有问题,因为父类原型对象复制得到的对象p中的constructor指向的不是subClass子类对象,因此在寄生式继承中要对复制的对象p做一次增强,修复其constructor指向不正确的问题,最后将得到的复制对象p赋给子类的原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。

修改之前的代码测试如下:


八 扩展——多继承

在其他的oo语言中,有多继承的概念,在JS中,我们也可以实现多继承,不过存在一定的局限性。我们先看一下继承单对象属性和extend方法。

function extend(target,source){
	for(var property in source){
		target[property] = source[property];
	}
	return target;
}

上面的extend是一个浅复制的过程,只能复制值类型的属性。对于引用类型无能为力。有了上面的属性复制,我们可以实现多个对象的属性的复制执行。

function mix(){
	var i = 1,
	len = arguments.length,
	target= arguments[0],
	arg;
	for(;i < len;i++){
		arg = arguments[i];
		for(var property in arg){
			target[property] = arg[property];
		}
	}
	return target;
}
上面这个方法也可以定义在原生对象上,这样所有的对象都可以使用了。
            

本文转载:CSDN博客