本文总结12种用于JavaScript实现继承的方法,它们大致上可以分为两类:
基于构造器工作的模式;
基于对象工作的模式;
此外,也可以基于以下条件对这些模式进行分类:
是否使用原型;
是否执行属性拷贝;
两者都有(即执行原型属性拷贝);
1.原型链法(仿传统)
基于构造器工作的模式 使用原型链模式
(可以将方法与属性集中可重用的部分迁移到原型链中,而将不可重用的的那部分设置为对象的自身属性)。示例代码如下:
function Shape () {}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.name;
}
function TwoDShape () {}
//原型对象扩展之前,先完成相关的继承关系构建
TwoDShape.prototype = new Shape()
/*
*完成相关的继承关系设定后,对这些对象的
*constructor属性进行相应的重置是一个非常好的习惯
*/
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.prototype.name = '2D shape';
function Triangle (side, height) {
/*
*因为由new Triangle()所创建的各个对象
*所表示的三角形在尺寸上各不相同,因此该对象的
*side和height这两个属性必须保持自身所有,而其他属性可以设置共享
*/
this.side = side;
this.height = height;
}
//扩展原型对象之前完成继承关系的构建
Triangle.prototype = new TwoDShape()
Triangle.prototype.constructor = Triangle;
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
//测试
var my = new Triangle(5, 10);
console.log(my.getArea());
console.log(my.toString());
//可以通过hasOwnProperty()方法来明确自身属性与其原型属性的区别
console.log(my.hasOwnProperty('side'));
console.log(my.hasOwnProperty('name'));
console.log(TwoDShape.prototype.isPrototypeOf(my));
console.log(my instanceof Shape);
2.仅从原型继承法
基于构造器工作的模式 原型拷贝模式(不存在原型链,所有的对象共享一个原型对象)
(这个模式在构建继承关系时不需要新建对象实例,效率上会有较好的表现;
原型链上的查询也会比较快,因为不存在原型链;缺点:子对象的修改会影响其父对象)。示例代码如下:
function Shape() {}
/*尽可能将一些可重用的属性和方法添加到原型中去,如果形成这样一个好习惯
*仅仅依靠原型就能完成继承关系的构建了。由于原型中的所有代码都是可重用的
*意味着继承自Shape.prototype比继承自new Shape()所创建的实体要好得多
*毕竟new Shape
*/
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape () {}
/*
*由于原型中的代码都是可重用的,意味着继承自Shape.prototype比继承自
*new Shape()所创建的实体要好得多.毕竟new Shape()方式会将Shape的属性设定
*为对象自身属性,这样的代码不可重用的(因而要将其设置在原型中)
*/
TwoDShape.prototype = Shape.prototype;//仅仅依靠原型就能完成继承了
TwoDShape.prototype.constructor = TwoDShape;//constructor属性进行重置
TwoDShape.prototype.name = '2D shape';
function Triangle (side, height) {
this.side = side;
this.height = height;
}
Triangle.prototype = TwoDShape.prototype;
Triangle.prototype.constructor = Triangle;//constructor属性进行重置
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
//测试
var my = new Triangle(5, 10);
console.log(my.getArea());
console.log(my.toString());
3.临时构造器法(与1号的区别在于父对象的自身属性不予继承)
基于构造器工作的模式 使用原型链模式
(只继承父对象的原型属性,这个模式还为访问父对象提供了便利的方式,即通过uber属性)
如上面所说,所有prototype属性都指向了一个相同的对象,父对象就会受到子对象属性的影响。要解决这个问题,利用某种中介来打破这种连锁关系。可以用一个临时构造器函数来充当中介。示例代码如下:
//将实现继承关系的代码提炼出来,既使代码保持简洁,又能将其重用在构建继承关系的任务中
function extend(Child, Parent) {
/*
*创建一个空函数F(),并将其原型设置为父级构造器。这样既可以用
*new F()来创建一些不包含父对象属性的对象,同时又可以从
*父对象prototype属性中继承一切了
*/
var F = function () {};//临时构造器函数来充当中介
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;//constructor属性重置
/*
*在构建继承关系的过程中引入uber属性,并令其指向其父级原型对象
*这样,子类就可以去调用父类中的同名方法了
*/
Child.uber = Parent.prototype;
}
function Shape () {}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.constructor.uber ? this.constructor.uber.toString()
+ ', ' + this.name : this.name;
};
function TwoDShape () {}
extend(TwoDShape, Shape);
TwoDShape.prototype.name = '2D Shape';
function Triangle (side, height) {
this.side = side;
this.height = height;
}
extend(Triangle, TwoDShape);
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
}
//测试
var my = new Triangle(5, 10);
console.log(my.getArea());
console.log(my.toString());
4.原型属性拷贝法
基于构造器工作的模式 拷贝属性模式 原型拷贝模式
(父对象原型中的内容全部转换成子对象原型属性,无须为继承单独创建对象实例,原型链本身也更短)
与之前的方法相比,这个方法在效率上略逊一筹。因为这里执行的是子对象原型逐一拷贝,而非简单的原型链查询。这种方式仅适用于只包含基本数据类型的对象,所有的对象类型(包括函数与数组)都是不可复制的,因为它们支持引用传递。示例代码如下:
function extend2 (Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
/*
*在构建可重用的继承代码时,可以简单地将父对象原型的所有属性
*拷贝给子对象的原型,其中包括方法,因为方法本身也是一种函数类型的属性
*/
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
/*
*由于这里已经完成对Child的原型进行扩展,不需要再去重置Child.prototype.constructor
*因为它不会再被完全覆盖了,因此这里constructor属性所指向的值是正确的
*/
}
Shape = function () {};
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
return this.uber ? this.uber.toString() + ', ' + this.name : this.name;
};
var TwoDShape = function () {};
//也会拷贝属于自己的toString()方法,但这只是一个函数引用,函数本身并没有被再次创建
extend2(TwoDShape, Shape);
//测试
var td = new TwoDShape();
console.log(td.__proto__.hasOwnProperty('name'));
console.log(td.__proto__.hasOwnProperty('toString'));
//这两个toString()方法实际上是同一个函数对象
console.log(td.__proto__.toString === Shape.prototype.toString);
console.log(td.toString());
TwoDShape.prototype.name = '2D shape';
console.log(td.toString());
5.全属性拷贝法(即浅拷贝法)
基于对象工作模式 属性拷贝模式
(没有使用原型属性)
这之前的方法都是以构造器创建对象为前提的,并且在这些用于创建对象的构造器中引入了从其他构造器中继承而来的属性。实际上,也可以丢开构造器,直接通过对象标识法来创建对象,这样做还能减少实际输入。示例代码如下:
function extendCopy(p) {
//创建一个没有任何私有属性的“空”对象作为“画板”,然后逐步为其添加属性
var c = {};
for (var i in p) {//将现有对象的属性全部拷贝过来
c[i] = p[i];
}
c.uber = p;
return c;
}
var shape = {
name: 'Shape',
toString: function () {
return this.name;
}
}
var twoDee = extendCopy(shape);
twoDee.name = '2D shape';
twoDee.toString = function () {
return this.uber.toString() + ', ' + this.name;
}
var triangle = extendCopy(twoDee);
triangle.name = 'Triangle';
triangle.getArea = function () {
return this.side * this.height / 2;
}
//测试
triangle.side = 5;
triangle.height = 10;
console.log(triangle.getArea());
console.log(triangle.toString());
6.深拷贝法
基于对象工作的模式 属性拷贝模式
浅拷贝,如果修改了拷贝对象,就等于修改了原对象;深拷贝可以避免这方面问题。实现方式与5号方法(浅拷贝)类似,但所有对象执行的都是值传递,即在遇到一个对象引用性的属性时,需要再次对其调用深拷贝函数。示例代码如下:
function extendCopy (p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
function deepCopy (p, c) {
c = c || {};
for (var i in p) {
if (p.hasOwnProperty(i)) {//确认不会拷贝不需要的继承属性
if (typeof p[i] === 'object') {
//遇到一个对象引用性的属性时,需要再次对其调用深拷贝函数
c[i] = Array.isArray(p[i]) ? [] : {};
deepCopy(p[i], c[i]);
}
else {
c[i] = p[i];
}
}
}
return c;
}
var parent = {
numbers: [1, 2, 3],
letters: ['a', 'b', 'c'],
obj: {
prop: 1
},
bool: true
}
//测试(对比深拷贝和浅拷贝)
var mydeep = deepCopy(parent);
var myshallow = extendCopy(parent);
mydeep.numbers.push(4, 5, 6);
console.log(mydeep.numbers);
console.log(parent.numbers);
myshallow.numbers.push(10);
console.log(myshallow.numbers);
console.log(parent.numbers);
console.log(mydeep.numbers);
7.原型继承法(将父对象设置成子对象的原型)
基于对象工作的模式 使用原型链模式
(丢开仿类机制,直接在对象之间构造继承关系。发挥原型固有优势)。示例代码如下:
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
var shape = {
name: 'Shape',
toString: function() {
return this.name;
}
}
var twoDee = extendCopy(shape);
twoDee.name = '2D shape';
twoDee.toString = function() {
return this.uber.toString() + ', ' + this.name;
}
// function object(o) {
// function F () {}
// F.prototype = o;
// return new F();
// }
/*
*如果需要访问uber属性,可以继续object()函数
*/
function object(o) { //接收父对象
var n;
function F() {}
F.prototype = o; //父对象为自对象原型
n = new F();
n.uber = o;
return n;
}
var triangle = object(twoDee);
triangle.name = 'Triangle';
triangle.getArea = function() {
return this.side * this.height / 2;
};
//测试
console.log(triangle.toString());
8.扩展与增强模式(原型继承与属性拷贝)
基于对象工作的模式 使用原型链模式 属性拷贝模式
(实际上是7号方法和5号方法的混合应用,通过一个函数一次性完成对象的继承与扩展)。示例代码如下:
对于继承来说,主要目标就是将一些现有的功能归为己有。也就是说在新建一个对象时,通常首先应该继承于现有对象,然后再为其添加额外的方法与属性。
function objectPlus (o,stuff) {//对象o用于继承,另一个对象stuff则用于拷贝方法与属性
var n;
function F () {}
F.prototype = o;//将已有对象设置为新对象的原型(原型继承的方式)
n = new F();
n.uber = o;
for (var i in stuff) {//将另一个已有对象的所有属性拷贝过来
n[i] = stuff[i];
}
return n;
}
var shape = {
name: 'shape',
toString: function () {
return this.name;
}
}
var twoDee = objectPlus(shape, {
name: '2D shape',
toString: function () {
return this.uber.toString() + ', ' + this.name;
}
});
var triangle = objectPlus(twoDee, {
name: 'Triangle',
getArea: function () {
return this.side * this.height / 2;
},
side: 0,
height: 0
});
//测试
var my = objectPlus(triangle, {
side: 4,
height: 4,
name: 'My 4x4'
});
console.log(my.getArea());
console.log(my.toString());
9.多重继承法
基于对象工作的模式 属性拷贝模式
(会按照父对象的出现顺序依次对它们执行属性全拷贝)
所谓的多重继承,通常指的是一个子对象中有不止一个父对象的继承模式。多重继承的实现很简单,只需要延续属性拷贝法(5号)的继承思路依次扩展对象即可,而对参数中所继承的对象的数量没有限制。示例代码如下:
function multi () {
var n = {}, stuff, len = arguments.length;
for (var i = 0; i < len; i++) {//外层循环用于遍历参数中传递进来的对象
stuff = arguments[i];
for (var j in stuff) {//内层循环用于拷贝属性
if (stuff.hasOwnProperty(j)) {
//如果传入的两个对象拥有同一个属性,前一个会被后一个覆盖掉
n[j] = stuff[j];
}
}
}
return n;
}
//测试
var shape = {
name: 'shape',
toString: function () {
return this.name;
}
};
var twoDee = {
name: '2D shape',
dimensions: 2
};
var triangle = multi(shape, twoDee, {
name: 'Triangle',
getArea: function () {
return this.side * this.height / 2;
},
side: 5,
height: 10
});
console.log(triangle.getArea());
console.log(triangle.dimensions);
console.log(triangle.toString());
10.寄生继承法
基于对象工作的模式 使用原型链模式
(该函数会执行相应的对象拷贝,并对其进行扩展,然后返回拷贝)
这种方法的基本思路是,可以在创建对象的函数中直接吸收其他对象的功能,然后对其进行扩展并返回。示例代码如下:
function object (o) {
var n;
function F () {}
F.prototype = o;
n = new F();
n.uber = o;
return n;
}
var twoD = {
name: '2D shape',
dimensions: 2
};
/*
*将twoD对象克隆进一个叫做that的对象,这一步可以使用之前的任何一种方法,
*例如使用object()函数或执行全属性拷贝。
*扩展that对象,添加更多的属性。返回that对象。
*/
function triangle(s, h) {//只是一个一般函数,不属于构造器
var that = object(twoD);//克隆一个叫that的对象
//扩展that对象,添加更多的属性
that.name = 'Triangle';
that.getArea = function () {
return this.side * this.height / 2;
};
that.side = s;
that.height = h;
return that;
}
//测试
var t = triangle(5, 10);
console.log(t.name);
console.log(t.dimensions);
console.log(t.getArea());
11.构造器借用法
基于构造器工作的模式
(这个方法可以只继承父对象的自身属性。可以与1号方法结合使用,以便从原型中继承相关内容,但是有个缺点:父对象的构造器会被调用两次。它便于子对象继承某个对象的具体属性时选择最简单处理方式)。
在这种继承模式中,子对象构造器可以通过call()或apply()方法来调用父对象的构造器,因而,它通常被称为构造器盗用法(stealing a constructor),或构造器借用法(borrowing a constructor)。正如我们所知,call()和apply()这两个方法都允许我们将某个指定对象的this值与一个函数的调用绑定起来。这对于继承而言,就意味着子对象的构造器在调用父对象的构造器,也可以将子对象中新建的this对象与父对象的this值绑定起来。示例代码如下:
//构造一个父类构造器Shape
function Shape(id) {
this.id = id;
}
Shape.prototype.name = 'shape';
Shape.prototype.toString = function () {
return this.name;
}
function Triangle () {
Shape.apply(this, arguments);//子对象可以通过call()或apply()方法来调用父对象
}
Triangle.prototype.name = 'Triangle';
//测试
var t = new Triangle(101);
console.log(t.name);
console.log(t.id);
console.log(t.toString());//这里新的triangle对象没有继承父对象原型中的任何东西
上面示例,之所以triangle对象中不包含Shape的原型属性,是因为没有调用new Shape()创建任何一个实例,自然其原型也从来没有被用到,这很容易做到,对Triangle()构造器进行如下重定义:
function Triangle () {
Shape.apply(this, arguments);
}
Triangle.prototype = new Shape();
Triangle.prototype.name = 'Triangle';
在这种继承模式中,父对象的属性是以自对象的自身属性的身份来重建的。这体现了构造器借用法的一个优势:创建一个继承于数组或者其他对象类型的子对象时,将获得一个完完全全的新值(不是一个引用),对它做任何修改都不影响父对象。
但这种模式也有一个缺点,因为这种情况下父对象的构造器往往会被调用两次:一次发生在通过apply()方法继承其自身属性时;另一次则发生在通过new操作符继承其原型时。这样一来父对象的自身属性被继承了两次。再看一个简单示例:
function Shape (id) {
this.id = id;
}
function Triangle () {
Shape.apply(this, arguments);
}
Triangle.prototype = new Shape(101);
//测试
var t = new Triangle(202);
console.log(t.id);//正如所见,对象中有一个自身id属性,但它并非来自原型链中
//验证
console.log(t.__proto__.id);
delete t.id;
console.log(t.id);
12.构造器借用与属性拷贝法
基于构造器工作的模式 原型链模式 属性拷贝模式
(本方法是11号方法与4号方法的结合体。它允许我们在不重复调用父对象构造器的情况下同时继承其自身属性和原型属性)
对11号方法中由于构造器的双重调用而带来的重复执行问题,很容易更正。可以在父对象构造器上调用apply()方法,以获得其全部的自身属性,然后再调用一个简单的迭代器对其原型属性进行逐项拷贝(也可以如使用4号方法来完成)。示例代码如下:
function extend2 (Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
//构造一个父类构造器Shape
function Shape (id) {
this.id = id;
}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
return this.name;
}
function Triangle () {
Shape.apply(this, arguments);
}
extend2(Triangle, Shape);//对父对象原型属性逐一拷贝
Triangle.prototype.name = 'Triangle';
//测试
var t = new Triangle(101);
console.log(t.toString());
console.log(t.id);
console.log(typeof t.__proto__.id);//输出为"undefined",这样双重继承已经不见了
console.log(t.uber.name);//如有必要,extend2()还可以访问对象的uber属性
面对以上这么多种继承方式,该如何做出正确的选择?事实上取决于开发者的设计风格、性能需求、具体项目任务。例如,如果开发者习惯于从类的角度来解决问题,那么基于构造器的工作模式适合。或者开发者更关心该“类”的某些具体实例,那么可能基于对象的模式更适合。