Javascript 继承篇
1. Prototype 链(Prototype chaining)
Javascript 是一种动态语言,实现一个目标通常有多种方式,继承也不例外。首先我们介绍下实现继承最普遍的方式 :利用 Prototype 链。
这里假设你已经对 prototype 以及 __proto__ 有了一定的了解,否则请先参考 Javascript 之 Prototype
prototype 链的示意图:
接下来的例子我们都采用类似示意图中的3层继承结构:最顶层为一个 Sharp 类,它有一个 名为 TwoDSharp 的子类,最后还有一个名为 Triangle 的类继承自 TwoDSharp。
1.1 Prototype 链示例
首先定义这3个类:
function Shape(){
this.name = ‘Shape‘;
this.toString = function () {
return this.name;
};
}
function TwoDShape(){
this.name = ‘2D shape‘;
}
function Triangle(side, height){
this.name = ‘Triangle‘;
this.side = side;
this.height = height;
this.getArea = function () {
return this.side * this.height / 2;
};
}
要实现继承关系,只需要添加下面的代码:
TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
// 修正 constructor 信息
TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;
这里需要指出: Javascript 操作的是对象而不是类,所以 prototype 属性需要一个实例化对象来实现继承,而不像其他 OO 语言直接声明继承自某一个类。
另外在你通过这种方法实现继承后,你再去修改甚至删除父类的函数,已经没有什么关系了,因为我们的继承关系是建立在实例对象上的。
var my = new Triangle(5, 10);
// 自属性
my.getArea();
// 25
// 父属性
my.toString();
// "Triangle"
// 修正 constructor 的语句让我们能获得正确的信息
my.constructor === Triangle;
// true
// 类型确认
my instanceof Shape;
// true
my instanceofTwoDShape;
// true
my instanceof Triangle;
// true
Shape.prototype.isPrototypeOf(my);
// true
TwoDShape.prototype.isPrototypeOf(my);
// true
Triangle.prototype.isPrototypeOf(my);
// true
1.2 把共有属性转到 prototype 上
当你用构造函数创建实例对象时,你可以通过 this 关键字来为其追加属性。当这些属性是只读时,这种做法值得商榷:
function Shape(){
this.name = ‘Shape‘;
}
这意味着你每一个 Shape 实例对象在内存中都保存着一份 name 属性的值,这显然是不必要的,可以尝试把属性转移到 prototype 上:
function Shape() { ... }
Shape.prototype.name = ‘Shape‘;
利用这种方法,我们可以改进一下前面的函数定义:
// 构造函数
function Shape() {}
// 扩展 prototype
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
return this.name;
};
// 构造函数
function TwoDShape() {}
// 继承实现
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
// 扩展 prototype
TwoDShape.prototype.name = ‘2D shape‘;
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// 继承实现
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
// 扩展 prototype
Triangle.prototype.name = ‘Triangle‘;
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
};
验证一下:
var my = new Triangle(5, 10);
my.getArea();
// 25
my.toString();
// "Triangle"
TwoDShape.prototype.isPrototypeOf(my);
// true
my instanceof Shape;
// true
输出结果都满足预期,这里要指出一个细节:my 上的 toString() 方法要比改进前多一步查找,因为原先这个函数就在 Shape 类上,而现在它在 Shape.prototype 上。
2. 仅继承 prototype
前面的例子可以看到在 prototype 上扩展可重用的属性和方法的优势,那么直接从 prototype 继承显然是个不错的方法,至少它带来两个好处:
- 不用再实例化对象来实现继承
- 运行时定位属性/方法的效率高了
function Shape() {}
// 扩展 prototype
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape() {}
// 实现继承
TwoDShape.prototype = Shape.prototype;
TwoDShape.prototype.constructor = TwoDShape;
// 扩展 prototype
TwoDShape.prototype.name = ‘2D shape‘;
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// 实现继承
Triangle.prototype = TwoDShape.prototype;
Triangle.prototype.constructor = Triangle;
// 扩展 prototype
Triangle.prototype.name = ‘Triangle‘;
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
};
// 测试
var my = new Triangle(5, 10);
my.getArea();
// 25
my.toString();
// "Triangle"
如果你细究一下,你会发现现在 my 的 toString() 函数定位高效了不少。
这里要提一下这个方法的缺点:由于父子的 prototype 都指向同一个对象,当其中一方修改时就会影响到另一方:
Triangle.prototype.name = ‘Triangle‘;
var s = new Shape();
s.name;
// "Triangle"
这显然不可接受啊!
3. 临时构造函数 – new F()
前面介绍的继承方法非常高效,却有个致命的缺点。我们引入一个临时的构造函数来解决:
function Shape() {}
// 扩展 prototype
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
return this.name;
};
function TwoDShape() {}
// 继承实现
var F = function () {};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
// 扩展 prototype
TwoDShape.prototype.name = ‘2D shape‘;
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// 继承实现
var F = function () {};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
// 扩展 prototype
Triangle.prototype.name = ‘Triangle‘;
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
};
// 测试
var s = new Shape();
s.name;
// "Shape"
"I am a " + new TwoDShape(); // 调用 toString()
// "I am a 2D shape"
3.1 子类中访问父类
传统的 OO 语言通常都有一个关键字(super/base等)来代表父类,这种方式下子类能非常方便的访问父类成员。
在 Javascript 中没有类似的语法,但我们依然能巧妙的实现类似的功能,下例中我们建立了一个 uber 属性来指向父的 prototype 对象。
function Shape() {}
// 扩展 prototype
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
var const = this.constructor;
return const.uber ? this.const.uber.toString() + ‘, ‘ + this.name : this.name;
};
function TwoDShape() {}
// 继承实现
var F = function () {};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.uber = Shape.prototype;
// 扩展 prototype
TwoDShape.prototype.name = ‘2D shape‘;
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// 继承实现
var F = function () {};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
Triangle.uber = TwoDShape.prototype;
// 扩展 prototype
Triangle.prototype.name = ‘Triangle‘;
Triangle.prototype.getArea = function () {
return this.side * this.height / 2;
};
这里修改了两个内容:
- 建立了一个 uber 属性来指向父类的 prototype 对象
- 一个新的 toString() 函数,用来帮助我们验证这套运行机制
var my = new Triangle(5, 10);
my.toString();
// "Shape, 2D shape, Triangle"
这里的属性名 uber 可以任意命名,但并不建议采用以下的常用名称。
“superclass”:这暗示了 Javascript 中有类的概念,而实际却不是这样
“super”:这是 Java 中的父对象关键字,在 Javascript 是保留字(虽然没作用)
3.2 把继承逻辑合到一个 function 中
现在我们把继承的实现逻辑提取出来做成一个 fucntion,方便调用:
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
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;
};
// 测试
new Triangle().toString();
// "Shape, 2D shape, Triangle"
4. 复制属性
现在换一下思路,既然我们继承的目的是重用代码,那么把一个对象的成员简单地复制到另一个对象不也可以吗?我们这就新建一个 extend2() 函数来实验一下这个方法:
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (vari in p) {
c[i] = p[i];
}
c.uber = p;
}
这里没有设置 Child.prototype.constructor 的原因是:我们没有整个替换掉 Child.prototype 对象,而是在其本身上进行了扩展。
这个方法比起前面的 extend() 来说有一个缺点:每个子类成员都被做了一个副本,而前者是通过 prototype 链。另外需要注意的是这个复制操作仅仅对原始(primitive)类型有效,对于 object 类型(包括 function 及 array)来说因为它们是基于引用传递的,你复制的仅仅是它们的地址!
var Shape = function () {};
varTwoDShape = function () {};
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
return this.uber ? this.uber.toString() + ‘, ‘ + this.name : this.name;
};
// extend()
extend(TwoDShape, Shape);
var td = new TwoDShape();
td.name;
// "Shape"
TwoDShape.prototype.name;
// "Shape"
td.__proto__.name;
// "Shape"
td.hasOwnProperty(‘name‘);
// false
td.__proto__.hasOwnProperty(‘name‘);
// false
// extend2()
extend2(TwoDShape, Shape);
var td = new TwoDShape();
td.__proto__.hasOwnProperty(‘name‘);
// true
td.__proto__.hasOwnProperty(‘toString‘);
// true
td.__proto__.toString === Shape.prototype.toString;
// true
子成员都需要做一个副本的话 extend2() 似乎并不高效,但这没有想象的那么糟糕因为实际上我们仅仅对原始(primitive)类型做了复制;另外当 Javascript 引擎在 prototype 链上查找成员时查找的路径变短了。
4.1 注意引用拷贝
有时候引用拷贝的结果并不是你期待的,比如下面的例子:
function Papa() {}
function Wee() {}
Papa.prototype.name = ‘Bear‘;
Papa.prototype.owns = ["porridge", "chair", "bed"];
extend2(Wee, Papa);
Wee.prototype.hasOwnProperty(‘name‘);
// true
Wee.prototype.hasOwnProperty(‘owns‘);
// true
Wee.prototype.owns;
// ["porridge", "chair", "bed"]
Wee.prototype.owns=== Papa.prototype.owns;
// true
Wee.prototype.name += ‘, Little Bear‘;
// "Bear, Little Bear"
Papa.prototype.name;
// "Bear"
// 对 Wee 的 owns 属性的修改会作用到 Papa 上
Wee.prototype.owns.pop();
// "bed"
Papa.prototype.owns;
// ["porridge", "chair"]
但是当你整个替换 Wee 的 owns 属性时,又是另外一番情形了:
Wee.prototype.owns= ["empty bowl", "broken chair"];
Papa.prototype.owns.push(‘bed‘);
Papa.prototype.owns;
// ["porridge", "chair", "bed"]
这里用一张示意图来分析一下这个过程:
- 生成一个新 object对象,A 中存放指向它的地址
- 声明一个新变量 B,把 A 中的地址复制一份给 B
- 修改 B.code 意味着修改 B 的地址指向内容的 code,由于 A 的指向和 B 的一致,所以修改也会作用到 A 上。
- 一个新对象被创建,B 的地址被改为指向这个新的对象。A 和 B 指向了不同的内容,不再互相影响了。
5. 从实例对象继承
前面的篇章我们都在构造函数上做文章,通过设置这些函数的 prototype 来实现继承。之前提到过 Javascript 中并没有类的概念,所以它的继承实际上就是代码的重用,前面的 extend2() 就是个比较直接的方式,但它只是在构造函数本身上动手脚。让我们更简单粗暴一点,跳过构造函数直接生成子类的实例对象岂不更好?
function extendCopy(p) {
var c = {};
for (vari in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
利用新的 extendCopy() 来实现一下前面的继承结构:
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;
triangle.getArea();
// 25
triangle.toString();
// "Shape, 2D shape, Triangle"
新的实现让我们不再需要定义子类的构造函数,但某些情形下你可能需要通过构造函数传入一些参数(譬如:创建 triangle 实例的同时传入 side,height),这要解决起来也是很简单的:
- 增加一个 init() 来传参数
- 在 extendCopy() 上添加第二个参数,用来传递这些信息
6. 深拷贝
你已经理解 Javascript 中的引用拷贝的内部原理:它们只是指针的拷贝,这被称之为浅拷贝。那么复制指针指向的内容就称之为深拷贝,它们的实现代码非常相似,深拷贝只是浅拷贝的一个“递归”版本:
function deepCopy(p, c) {
c = c || {};
for (vari 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);
// 6
mydeep.numbers;
// [1, 2, 3, 4, 5, 6]
parent.numbers;
// [1, 2, 3]
myshallow.numbers.push(10);
// 4
myshallow.numbers;
// [1, 2, 3, 10]
parent.numbers;
// [1, 2, 3, 10]
mydeep.numbers;
// [1, 2, 3, 4, 5, 6]
最后补充两点:
- 使用 hasOwnProperty() 来排除非自有属性,以此保证不会附加多余的属性
- Array.isArray() 是 ES5 的规范,如果你的 Javascript 环境中没有别惊慌,使用下面的 Polyfill:
if (Array.isArray !== "function") {
Array.isArray = function (candidate) {
return Object.prototype.toString.call(candidate) === ‘[object Array]‘;
};
}
7. object()
基于从对象继承的模式,有人提出使用一个 object() 函数来实现:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
如果需要对父操作,可以改为:
// 虽然是从实例对象继承,但这种模式内部使用 prototype 继承的方式
function object(o) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
return n;
}
使用起来与 extendCopy() 一样,也是传入一个实例对象:
var triangle = object(twoDee);
triangle.name = ‘Triangle‘;
triangle.getArea = function () {
return this.side * this.height / 2;
};
triangle.toString();
// "Shape, 2D shape, Triangle"
// 在 ES5 中内置了类似的函数 Object.create()
var square = Object.create(triangle);
8. prototype 继承 与 成员拷贝的混合使用
继承的时候,你总是想能够利用既有的功能,以此为基础进行扩展,同时利用前面的两种继承方式可以让我们很容易达成该目的。
- 使用 prototype 继承来获取既有对象的功能
- 使用成员拷贝来扩展新的实例对象
function objectPlus(o, stuff) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (vari in stuff) {
n[i] = stuff[i];
}
return n;
}
objectPlus() 函数的第一个参数用来 prototype 继承,第二个参数用来扩展,来看看具体的使用:
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
});
my.getArea();
// 8
my.toString();
// "Shape, 2D shape, Triangle, Triangle"
最后的输出出现了两个 Triangle,这是因为实例对象 my 继承自 Triangle,导致继承层数多了一层,如果创建时给 name 赋个值就能分辨出这个原由了:
objectPlus(triangle, {
side: 4,
height: 4,
name: ‘My 4x4‘
}).toString();
// "Shape, 2D shape, Triangle, My 4x4"
objectPlus() 与 ES5 中的 Object.create() 非常相似,唯一不同的是第二个参数。详细可参考相关的文档。
9. 多重继承
多重继承就是子类有多个父类,在 OO 语言中许多都不支持,当你遇到是否引入多重继承时需要考虑再三,因为它可能给你带来便利的同时增加程序的复杂度,同时打断直观的继承链。
采用成员拷贝的做法能够很容易地实现多重继承,你甚至可以继承自无限多个父对象。下面的 multi() 函数接受一个父对象数组,内部使用 arguments 关键字来遍历这个数组,并逐一复制:
function multi() {
var n = {}, stuff, j = 0, len = arguments.length;
for (j = 0; j <len; j++) {
stuff = arguments[j];
for (vari in stuff) {
if (stuff.hasOwnProperty(i)) {
n[i] = stuff[i];
}
}
}
return n;
}
// 使用
var shape = {
name: ‘Shape‘,
toString: function () {
return this.name;
}
};
vartwoDee = {
name: ‘2D shape‘,
dimensions: 2
};
var triangle = multi(shape, twoDee, {
name: ‘Triangle‘,
getArea: function () {
return this.side * this.height / 2;
},
side: 5,
height: 10
});
// 测试
triangle.getArea();
// 25
triangle.dimensions;
// 2
triangle.toString();
// "Triangle"
multi() 采用顺序遍历的方式复制类成员,所以如果父对象的成员有相同的名字,最后出现的作用在子对象上
混合体(Mixins)
混合体是一种多个对象的合体,它通常具有所有这些合体对象的功能(成员),但本质上并不是这些对象的子对象。(前文的 multi() 生成的结果就是一个混合体)
10. 寄生式继承(Parasitic inheritance)
寄生式继承:把一个对象上的所有功能移到新的实例对象上,并对新的实例对象进行扩展。
var twoD = {
name: ‘2D shape‘,
dimensions: 2
};
// 寄生式继承
function triangle(s, h) {
// 使用 object() 来拷贝对象上的所有成员
var that = object(twoD);
// 修改/扩展
that.name =‘Triangle‘;
that.getArea = function () {
return this.side * this.height / 2;
};
that.side = s;
that.height = h;
// 返回新实体
return that;
}
使用时注意不用 new 关键字:
var t = triangle(5, 10);
t.dimensions;
// 2
vart2 = new triangle(5,5);
t2.getArea();
// 12.5
11. 构造函数的借用
这是本文介绍的最后一种实现继承的方式:子对象使用 call() 或 apply() 来调用父对象的构造函数。
如果你对 call() 或 apply() 不是非常了解,这里简短的介绍一下:
这两个方法让你调用函数的时候传入一个对象,并把该函数中的 this 与传入的这个对象进行绑定。
// 父
function Shape(id) {
this.id = id;
}
Shape.prototype.name = ‘Shape‘;
Shape.prototype.toString = function () {
return this.name;
};
// 子
function Triangle() {
Shape.apply(this, arguments);
}
Triangle.prototype.name = ‘Triangle‘;
// 测试
var t = new Triangle(101);
t.name;
// "Triangle"
t.id;
// 101
t.toString();
// "[object Object]"
实例 Triangle 对象从 Shape 上继承了 id 属性,但是并没有得到父的 prototype 上的内容。根据前文的介绍,我们知道可以这样修改来解决这个问题:
// 子
function Triangle() {
Shape.apply(this, arguments);
}
Triangle.prototype = new Shape();
Triangle.prototype.name = ‘Triangle‘;
这里有个小问题问题:父的构造函数被调用了两次,一次是 apply() 发起的,一次是设置 prototype 的时候。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。