JavaScript之继承

继承

许多OO语言都支持两种继承方式: 接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
如前所述,由于函数没有签名,在ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

原型链

将原型链作为实现继承的主要方法,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

简单回顾一下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

假如我们让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。

假如另一个原型又是另一个类型的实例,那么上述关系依旧成立,这样层层递进,就构成了实例与原型的链条。

function SuperType() {
	this.property = true;
}

SuperType.prototype.getSuperValue = function() {
	return this.property;
}

//继承了 SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
 return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); //true 

实现的本质是重写原型对象,代之以一个新类型的实例。

原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType中了。确立了继承关系后,还可以给SubType.prototype添加方法,这样就在继承的基础上又新增了一个方法。

最终结果是:

instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()
方法仍然还在SuperType.prototype 中,但 property 则位于 SubType.prototype 中。
此外,还要注意instance.constructor现在指向SuperType。

1、别忘记默认的原型

事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了
Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是 Object
的实例,因此默认原型都会包含一个内部指针,指向
Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。

  • 上面的那个例子,SubType继承了SuperType,而SuperType继承了Object。

  • 当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。

2、确定原型和实例的关系

  • 可以通过两种方式来确定原型和实例之间的关系。

  • 第一种方式使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true

alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true 
  • 第二种方式使用isPrototypeOf()方法,同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true 

3、谨慎地定义方法

  • 子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

  • 如果给SubType的原型添加或者重写方法,而后再替换原型为SuperType,那么之前进行的改动都将失效,所以记住,一定要先继承完毕,再添加修改。

注意:

在通过原型链实现继承时,不能使用对象字面量创建原型方法,否则会重写原型链。宁愿一遍一遍地敲prototye添加属性和方法,也不要用简单原型语法中的一个对象字面量全部概括。

4、原型链的问题

  • 原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。

  • 其中,最主要的问题来自包含引用类型值的原型。

  • 想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。

  • 而在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。

借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor
stealing)的技术(有时候也叫做伪造对象或经典继承)。

这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。 别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用
apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数,

function SuperType(){
	this.colors = ["red", "blue", "green"];
}

function SubType(){
	//继承了 SuperType
	SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

SubType中的那一行代码“借调”了超类型的构造函数。通过使用 call()方法(或
apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType
构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType
的每个实例就都会具有自己的 colors 属性的副本了。

1、传递参数

  • 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){
 this.name = name;
}

function SubType(){
 //继承了 SuperType,同时还传递了参数
 SuperType.call(this, "Nicholas");

 //实例属性
 this.age = 29;
}

var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29 

以上代码中的 SuperType 只接受一个参数 name,该参数会直接赋给一个属性。在 SubType 构造函数内部调用
SuperType 构造函数时,实际上是为 SubType 的实例设置了 name 属性。为了确保SuperType
构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

2、借用构造函数的问题

  • 如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。

  • 而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

  • 考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承

也叫伪经典继承,将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。

function SuperType(name) {
	this.name = name;
	this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
	alert(this.name);
}

function SubType(name, age) {
	// 继承属性
	SuperType.call(this, name);
	this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
	alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27 

在这个例子中,SuperType 构造函数定义了两个属性:name 和 colors。SuperType 的原型定义了一个方法
sayName()。SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,紧接着又定义了它自己的属性
age。然后,将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法
sayAge()。这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

原型式继承

一种实现继承的方法: 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o){
 function F(){}
 F.prototype = o;
 return new F();
}

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制

  • 这种原型式继承,要求必须要有一个对象可以作为另一个对象的基础

  • 用这种方式创建的对象相当于是传入参数对象的副本

ES5新增了Object.create()方法规范化了原型式继承。这个方法接受两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。

  • Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

  • IE8及以下不支持Object.create()方法

  • 在只想让一个对象与另一个对象保持类似的情况下,原型继承是完全可以胜任的。不过别忘了,原型模式下的缺点:引用类型属性的共享问题。

寄生式继承

寄生式继承与原型式继承紧密相关,与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

function createAnother(original) {
	var clone = object(original); //通过调用函数创建一个新对象
	clone.sayHi = function(){		// 以某种方式来增强这个对象
		alert("hi");
	};
	return clone;			// 返回这个对象
}
  • 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

寄生组合式继承

前面说过,组合继承是 JavaScript
最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

回顾一下组合继承:

function SuperType(name){
	this.name = name;
	this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
	alert(this.name);
};

function SubType(name, age){
	SuperType.call(this, name);			//第二次调用 SuperType()

	this.age = age;
}

SubType.prototype = new SuperType();	//第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
	alert(this.age);
}; 

在第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们都是SuperType 的实例属性,只不过现在位于 SubType 的原型中。

当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。

  • 因为使用组合继承,调用了两次SuperType构造函数,现在出现了两组属性,一组在实例上,一组在SubType原型中。

  • 解决这个问题的办法就是使用寄生组合式继承:

    • 借用构造函数来继承属性,通过原型链的混成形式来继承方法。

    • 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType, superType) {
	var prototype = object(superType.prototype);	//创建对象
	prototype.constructor = subType;				//增强对象
	subType.prototype = prototype;					//指定对象
}
  • 在函数内部,第一步是创建超类型原型的一个副本。

  • 第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。

  • 最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了

function SuperType(name){
	this.name = name;
	this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
	alert(this.name);
};

function SubType(name, age){
	SuperType.call(this, name);
	this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
	alert(this.age);
};

只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还保持不变,因此能够正常使用instanceof和isPrototypeOf(),开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

小结

ECMAScript 支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。

  • 工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。

  • 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。

  • 原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。

  • 组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。

JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。

  • 原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。

  • 解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能往超类型构造函数中传递参数,但是没有函数复用。

  • 使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

  • 此外,还存在下列可供选择的继承模式。

    • 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。

    • 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。

    • 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值