Javascript 继承篇

来源:互联网 发布:国泰安数据库 登陆 编辑:程序博客网 时间:2024/05/22 12:52

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;// truemy instanceofTwoDShape;// truemy instanceof Triangle;// trueShape.prototype.isPrototypeOf(my);// trueTwoDShape.prototype.isPrototypeOf(my);// trueTriangle.prototype.isPrototypeOf(my);// true

1.2 把共有属性转到 prototype 上

当你用构造函数创建实例对象时,你可以通过 this 关键字来为其追加属性。当这些属性是只读时,这种做法值得商榷:

function Shape(){  this.name = 'Shape';}

这意味着你每一个 Shape 实例对象在内存中都保存着一份 name 属性的值,这显然是不必要的,可以尝试把属性转移到 prototype 上:

function Shape() { ... }Shape.prototype.name = 'Shape';

利用这种方法,我们可以改进一下前面的函数定义:

// 构造函数function Shape() {}// 扩展 prototypeShape.prototype.name = 'Shape';Shape.prototype.toString = function () {  return this.name;};// 构造函数function TwoDShape() {}// 继承实现TwoDShape.prototype = new Shape();TwoDShape.prototype.constructor = TwoDShape;// 扩展 prototypeTwoDShape.prototype.name = '2D shape';function Triangle(side, height) {  this.side = side;  this.height = height;}// 继承实现Triangle.prototype = new TwoDShape();Triangle.prototype.constructor = Triangle;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () {  return this.side * this.height / 2;};

验证一下:

var my = new Triangle(5, 10);my.getArea();// 25my.toString();// "Triangle"TwoDShape.prototype.isPrototypeOf(my);// truemy instanceof Shape;// true

输出结果都满足预期,这里要指出一个细节:my 上的 toString() 方法要比改进前多一步查找,因为原先这个函数就在 Shape 类上,而现在它在 Shape.prototype 上。

2. 仅继承 prototype

前面的例子可以看到在 prototype 上扩展可重用的属性和方法的优势,那么直接从 prototype 继承显然是个不错的方法,至少它带来两个好处:

  • 不用再实例化对象来实现继承
  • 运行时定位属性/方法的效率高了
function Shape() {}// 扩展 prototypeShape.prototype.name = 'Shape';Shape.prototype.toString = function () {  return this.name;};function TwoDShape() {}// 实现继承TwoDShape.prototype = Shape.prototype;TwoDShape.prototype.constructor = TwoDShape;// 扩展 prototypeTwoDShape.prototype.name = '2D shape';function Triangle(side, height) {  this.side = side;  this.height = height;}// 实现继承Triangle.prototype = TwoDShape.prototype;Triangle.prototype.constructor = Triangle;// 扩展 prototypeTriangle.prototype.name = 'Triangle';Triangle.prototype.getArea = function () {  return this.side * this.height / 2;};// 测试var my = new Triangle(5, 10);my.getArea();// 25my.toString();// "Triangle"

如果你细究一下,你会发现现在 my 的 toString() 函数定位高效了不少。
这里要提一下这个方法的缺点:由于父子的 prototype 都指向同一个对象,当其中一方修改时就会影响到另一方:

Triangle.prototype.name = 'Triangle';var s = new Shape();s.name;// "Triangle"

这显然不可接受啊!

3. 临时构造函数 – new F()

前面介绍的继承方法非常高效,却有个致命的缺点。我们引入一个临时的构造函数来解决:

function Shape() {}// 扩展 prototypeShape.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;// 扩展 prototypeTwoDShape.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;// 扩展 prototypeTriangle.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() {}// 扩展 prototypeShape.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;// 扩展 prototypeTwoDShape.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;// 扩展 prototypeTriangle.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');// falsetd.__proto__.hasOwnProperty('name');// false// extend2()extend2(TwoDShape, Shape);var td = new TwoDShape();td.__proto__.hasOwnProperty('name');// truetd.__proto__.hasOwnProperty('toString');// truetd.__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');// trueWee.prototype.hasOwnProperty('owns');// trueWee.prototype.owns;// ["porridge", "chair", "bed"]Wee.prototype.owns=== Papa.prototype.owns;// trueWee.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"]

这里用一张示意图来分析一下这个过程:
这里写图片描述

  1. 生成一个新 object对象,A 中存放指向它的地址
  2. 声明一个新变量 B,把 A 中的地址复制一份给 B
  3. 修改 B.code 意味着修改 B 的地址指向内容的 code,由于 A 的指向和 B 的一致,所以修改也会作用到 A 上。
  4. 一个新对象被创建,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();// 25triangle.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);// 6mydeep.numbers;// [1, 2, 3, 4, 5, 6]parent.numbers;// [1, 2, 3]myshallow.numbers.push(10);// 4myshallow.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();// 8my.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();// 25triangle.dimensions;// 2triangle.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;// 2vart2 = 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;// 101t.toString();// "[object Object]"

实例 Triangle 对象从 Shape 上继承了 id 属性,但是并没有得到父的 prototype 上的内容。根据前文的介绍,我们知道可以这样修改来解决这个问题:

// 子function Triangle() {  Shape.apply(this, arguments);}Triangle.prototype = new Shape();Triangle.prototype.name = 'Triangle';

这里有个小问题问题:父的构造函数被调用了两次,一次是 apply() 发起的,一次是设置 prototype 的时候。

0 0
原创粉丝点击