JavaScript之对象继承

30次阅读

共计 4029 个字符,预计需要花费 11 分钟才能阅读完成。

JavaScript 对象继承的方法有很多,这里总结一下几种比较常用的方法。
现在有一个 ” 动物 ” 对象的构造函数。

function Animal(){this.species = "动物";}
Animal.prototype.voice = function(){console.log('voice');
}

还有一个 ” 猫 ” 对象的构造函数。

function Cat(name, color){
    this.name = name;
    this.color = color;
}

怎样才能使 ” 猫 ” 继承 ” 动物 ” 呢?

一、原型链和构造函数继承

使用 call/apply 和 Object.create()

第一种方法使用 call 或 apply 方法,改变了 this 的指向而实现继承,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Cat(name, color){Animal.apply(this, arguments); 
    this.name = name;
    this.color = color;
}
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

到目前为止一切看起来都还行,但是我们很快会发现我们遇到问题了,当我们调用 cat1.voice()的时候报错了,显然 Cat 并没有继承 Animal 的原型对象里的方法。

我们已经定义了一个新的构造器,这个构造器默认有一个空的原型属性。我们并没有让 Cat 从 Animal 的原型对象里继承方法。

Cat.prototype = Object.create(Animal.prototype);

这里我们的老朋友 create()又来帮忙了——在这个例子里我们用这个函数来创建一个和 Animal.prototype 一样的新的原型属性值(这个属性指向一个包括属性和方法的对象),然后将其作为 Cat.prototype 的属性值。这意味着 Cat.prototype 现在会继承 Animal.prototype 的所有属性和方法。

接下来,在我们动工之前,还需要完成一件事 — 现在 Cat()的 prototype 的 constructor 属性指向的是 Aniaml(),这是由我们生成 Animal()的方式决定的。(这篇 Stack Overflow post 文章会告诉您详细的原理)

Cat.prototype.constructor = Cat;

此时再输入 Cat.prototype.constructor 就会得到 Cat()。

顺带一提,这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了 prototype 对象,那么,下一步必然是为新的 prototype 对象加上 constructor 属性,并将这个属性指回原来的构造函数。

    o.prototype = {};
    o.prototype.constructor = o;

对上面构造函数继承代码做一个封装:

function extend(C, P) {function C(){P.apply(this, arguments); 
    }
    C.prototype = Object.create(P.prototype);
    C.prototype.constructor = C;
}

不使用 call 或 apply 方法

第二种方法更常见,但是不使用 apply 和 call,而是使用 prototype 属性。

如果 ” 猫 ” 的 prototype 对象,指向一个 Animal 的实例,那么所有 ” 猫 ” 的实例,就能继承 Animal 了。

    var Cat = function(){};
    Cat.prototype = new Animal();
    Cat.prototype.constructor = Cat;

    var cat1 = new Cat("大毛","黄色");
    console.log(cat1.species); // => 动物

代码的第一行,我们将 Cat 的 prototype 对象指向一个 Animal 的实例。
它相当于完全删除了 prototype 对象原先的值,然后赋予一个新值。但是此时 Cat.prototype.constructor 依然指向 Animal 的构造函数,所以需要将其指回 Cat。

对 prototype 继承代码做一个封装:

function extend(C, P) {var C = function(){};
    C.prototype = new P();
    C.prototype.constructor = C;
}

原型式继承(prototypal inheritance)(推荐)

利用一个空对象作为中介。

var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;

F 是空对象,所以几乎不占内存。这时,修改 Cat 的 prototype 对象,就不会影响到 Animal 的 prototype 对象。

alert(Animal.prototype.constructor); // Animal

我们将上面的方法,封装成一个函数,便于使用。

function extend(C, P) {var F = function(){};
    F.prototype = Parent.prototype;
    C.prototype = new F();
    C.prototype.constructor = Child;
    C.super = P.prototype;
}

使用的时候,方法如下

  extend(Cat,Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

这个 extend 函数,就是 YUI 库如何实现继承的方法。

另外,说明一点,函数体最后一行

  Child.super = Parent.prototype;

意思是为子对象设一个 super 属性,这个属性直接指向父对象的 prototype 属性。这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

“ 非构造函数 ” 的继承

什么是 ” 非构造函数 ” 的继承?

比如,现在有一个对象,叫做 ” 中国人 ”。

  var Chinese = {nation:’中国 '};

还有一个对象,叫做 ” 医生 ”。

  var Doctor ={career:’医生’}

请问怎样才能让 ” 医生 ” 去继承 ” 中国人 ”,也就是说,我怎样才能生成一个 ” 中国医生 ” 的对象?

这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现 ” 继承 ”。

深拷贝继承(推荐)

提到拷贝继承,不得不提到浅拷贝,浅拷贝只能拷贝一层深度,但是很多时候我们碰到的对象结构不止一层,所以浅拷贝并不适合我们实际开发中的使用,这里也就不多做说明了。

所谓 ” 深拷贝 ”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要 递归 调用 ” 浅拷贝 ” 就行了。

  function deepCopy(p, c) {var c = c || {};
    for (var i in p) {if (typeof p[i] === 'object') {c[i] = (p[i].constructor === Array) ? [] : {};
        deepCopy(p[i], c[i]);
      } else {c[i] = p[i];
      }
    }
    return c;
  }

使用的时候这样写:

  var Doctor = deepCopy(Chinese);

现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:

  Chinese.birthPlaces = ['北京','上海','香港'];
  Doctor.birthPlaces.push('厦门');

这时,父对象就不会受到影响了。

  alert(Doctor.birthPlaces); // 北京, 上海, 香港, 厦门
  alert(Chinese.birthPlaces); // 北京, 上海, 香港

目前,jQuery 库使用的就是这种继承方法。

extends 关键字实现继承

这个是 ES6 的语法糖,下面看下 es6 实现继承的方法

class Parent {constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Children extends Parent {constructor(name, age, job) {
    this.job = job; // 这里会报错
    super(name, age);
    this.job = job; // 正确
  }
}

上面代码中,子类的 constructor 方法没有调用 super 之前,就使用 this 关键字,结果报错,而放在 super 方法之后就是正确的。子类 Children 的构造函数之中的 super(),代表调用父类 Parent 的构造函数。这是必须的,否则 JavaScript 引擎会报错。

注意,super 虽然代表了父类 Parent 的构造函数,但是返回的是子类 Children 的实例,即 super 内部的 this 指的是 Children,因此 super()在这里相当于 Parent.prototype.constructor.call(this)。


参考:

  1. http://www.ruanyifeng.com/blo…
  2. https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance


推荐阅读:
【专题:JavaScript 进阶之路】
JavaScript 之深入理解闭包
ES6 尾调用和尾递归
Git 常用命令小结
JavaScript 之 call() 理解
JavaScript 之对象属性


我是 Cloudy,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流前端各种问题!

正文完
 0