7.核心JavaScript笔记:面向对象(继承)

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法

由于函数没有签名,ECMAScript 只支持实现继承,它的继承主要是依靠原型链来实现的

一. 原型链

原型链是实现继承的主要方法

原型链的基本概念是——利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条

实现原型链有一种基本模式,其代码大致如下

function SuperType(){ 
	this.property = true; 
} 
SuperType.prototype.getSuperValue = function(){ 
	return this.property; 
}; 
function SubType(){ 
	this.subproperty = false; 
} 
//继承了SuperType 
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){ 
	return this.subproperty; 
}; 
var instance = new SubType(); 
alert(instance.getSuperValue());    //true 

实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了

1. 别忘记默认的原型

事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的

所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()等默认方法的根本原因

3. 确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true

第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型

4. 谨慎地定义方法

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

还有一点需要注意,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链

5. 原型链的问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型

包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,实例会变成另一个类型的原型。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数

二. 借用构造函数

为了解决原型中包含引用类型值所带来的问题,可以使用一种叫做借用构造函数的技术(也叫伪造对象或经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数

因为,函数只不过是在特定环境中执行代码的对象,因此通过使用 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"

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

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);
};

这样一来,两个不同的SubType实例既分别拥有自己属性——包括colors属性,又可以使用相同的方法了

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

四. 原型式继承

原型式继承并没有使用严格意义上的构造函数

基本思想:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

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

从本质上讲,object()对传入其中的对象执行了一次浅复制

var person = {
	name: "strong",
	friends: ["a", "b", "c"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("d");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("e");
alert(person.friends); //"a,b,c,d,e"

person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上相当于又创建了 person 对象的两个副本

Object.create()方法

ECMAScript5新增的Object.create() 方法规范化了原型式继承,接收两个参数

  • 一个用作新对象原型的对象
  • (可选的)一个为新对象定义额外属性的对象

在传入一个参数的情况下,Object.create() 与 object() 方法的行为相同,第二个参数就是一个属性描述符对象

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样

五. 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

function createAnother(original){
	var clone = object(original); //通过调用函数创建一个新对象
	clone.sayHi = function(){ //以某种方式来增强这个对象
		alert("hi");
	};
	return clone; //返回这个对象
}

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object() 函数不是必需的;任何能够返回新对象的函数都适用于此模式

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似

六. 寄生组合式继承

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

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);  //第2次调用 SuperType();
	this.age = age;
}
SubType.prototype = new SuperType(); //第1次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
	alert(this.age);
};

有两组 name 和 colors 属性:一组在实例上,一组在 SubType 原型中。这就是调用两次 SuperType 构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

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()

寄生组合式继承是引用类型最理想的继承范式

你可能感兴趣的:(JavaScript)