JavaScript 实现继承共 6 种方式

原型链继承

通过原型继承多个引用类型的属性和方法。
一个构造函数的原型是另一个构造函数的实例。

  1. function SuperType() {
  2. this.property = true;
  3. }
  4. SuperType.prototype.getSuperValue = function() {
  5. return this.property;
  6. };
  7. function SubType() {
  8. this.subproperty = false;
  9. }
  10. // 继承SuperType
  11. SubType.prototype = new SuperType();
  12. SubType.prototype.getSubValue = function() {
  13. return this.subproperty;
  14. };
  15. let instance = new SubType();
  16. console.log(instance.getSuperValue()); // true

原型与实例的关系可以通过两种方式来确定。
第一种方式是使用 instanceof 操作符,如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true

  1. console.log(instance instanceof Object); // true
  2. console.log(instance instanceof SuperType); // true
  3. console.log(instance instanceof SubType); // true

第二种方式是使用 isPrototypeOf() 方法。原型链中的每个原型都可以调用这个方法。

  1. console.log(Object.protptype.isPrototypeOf(instance)); // true
  2. console.log(SuperType.prototype.isPrototypeOf(instance)); // true
  3. console.log(SubType.prototype.isPrototypeOf(instance)); // true

instanceof 与 isPrototypeOf 的区别:
isPrototypeOf 用来检测一个对象是否存在于另一个对象的原型链上,如果存在就返回 true ,否则就返回 false 。
instanceof 用于检测构造函数的 prototyp 属性是否出现在某个实例对象的原型链上。

原型链继承的问题:

  • 原型中包含的引用值会在所有实例间共享。 ```javascript function SuperType() { this.colors = [“red”, “blue”, “green”]; }

function SubType() {}

// 继承SuperType SubType.prototype = new SuperType();

let instance1 = new SubType(); instance1.colors.push(“black”); console.log(instance1.colors); // “red,blue,green,black”

let instance2 = new SubType(); console.log(instance2.colors); // “red,blue,green,black”

  1. - 子类型在实例化时不能给父类型的构造函数传参。
  2. <a name="edoUa"></a>
  3. # 借用构造函数继承
  4. 在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 `apply` `call` 方法以新创建的对象为上下文执行构造函数。
  5. ```javascript
  6. function SuperType() {
  7. this.colors = ["red", "blue", "green"];
  8. }
  9. function SubType() {
  10. // 继承SuperType
  11. SuperType.call(this);
  12. }
  13. let instance1 = new SubType();
  14. instance1.colors.push("black");
  15. console.log(instance1.colors); // "red, blue, green, black"
  16. let instance2 = new SubType();
  17. console.log(instance2.colors); // "red, blue, green"

优点:

  1. 可以向父类传递参数
  2. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

  1. 无法继承原型中的方法
  2. 实例并不是父类的实例,只是子类的实例
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
    1. function Animation(name, age) {
    2. this.name = name;
    3. this.age = age;
    4. }
    5. Animation.prototype.eat = function() {
    6. console.log(this.name);
    7. }
    8. function Cat(name, age, sex, score) {
    9. Animation.call(this, name, age);
    10. this.sex = sex;
    11. this.score = score;
    12. }
    13. var c1 = new Cat("tom", 18, "男", 100)
    14. c1.cat() // TypeError
    15. console.log(c1 instanceof Animation) // false
    16. console.log(c1 instanceof Cat) // true

    组合继承

    综合了原型链和构造函数,将两者的优点集中了起来。基本的思想是使用原型链继承原型上的属性和方法,而通过借用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。 ```javascript function SuperType(name) { this.name = name; this.colors = [“red”, “blue”, “green”]; }

SuperType.prototype.sayName = function() { console.log(this.name); };

function SubType(name, age) { SuperType.call(this, name); this.age = age; }

// 继承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function() { console.log(this.age); } let instance1 = new SubType(“Nicholas”, 29); instance1.colors.push(“black”); console.log(instance1.colors); // “red,blue,green,black” instance1.sayName(); // “Nicholas” instance1.sayAge(); // 29

let instance2 = new SubType(“Greg”, 27); console.log(instance2.colors); // “red, blue, green” instance2.sayName(); // “Greg” instance2.sayAge(); // 27

  1. 特点:
  2. - 可以继承父类原型上的属性,可以传参,函数可复用。
  3. - 每个新实例引入的构造函数属性是私有的。
  4. 缺点:
  5. - 调用了两次父类构造函数(耗内存)(一次是在创建子类型原型的时候,另一次是在子类型构造函数内部),子类的构造函数会代替原型上的那个父类构造函数。
  6. <a name="iRWUj"></a>
  7. # 原型式继承
  8. 即使不自定义类型也可以通过原型实现对象之间的信息共享。
  9. ```javascript
  10. function object(o) {
  11. function F() {};
  12. F.prototype = o;
  13. return new F();
  14. }

ES6 通过增加 Object.create() 方法将原型式继承的概念规范化了。
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

  1. function createAnother(original) {
  2. let clone = object(original); // 通过调用函数创建一个新对象
  3. clone.sayHi = function() { // 以某种方式增强这个对象
  4. console.log("hi");
  5. };
  6. return clone; // 返回这个对象
  7. }
  8. let person = {
  9. name: "Nicholas",
  10. fridends: ["Shelby", "Court", "Van"]
  11. };
  12. let anotherPerson = createAnother(person);
  13. anotherPerson.sayHi(); // 'hi'

在考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下
  • 同原型链实现继承一样,包含引用类型值得属性会被所有实例共享。
  • 没用到原型,无法复用。

    寄生组合式继承

    本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
    寄生式组合继承通过构造函数继承属性,但是用混合式原型链继承方法,基本思路是不通过父类构造函数给子类原型赋值,而是取得父类原型的一个副本,说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。寄生式组合继承的基本模式如下所示: ```javascript function inheritPrototype(subType, superType) { let prototype = object(superType.prototype); // 创建对象 prototype.constructor = subType; // 增强对象 subType.prototype = prototype; // 赋值对象 }

function SuperType(name) { this.name = name; this.colors = [“red”, “blue”, “green”]; } SuperType.prototype.sayName = function() { console.log(this.name); };

function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { console.log(this.age); };

  1. 优点:
  2. - 只调用了一次超类构造函数,效率更高。
  3. - 避免在 SuperType.prototype 上面创建不必要得、多余得属性,与此同时,原型链还能保持不变。
  4. <a name="C4HsF"></a>
  5. # ES5继承 VS ES6继承
  6. 1. ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(`Parent.apply(this)`
  7. 2. ES6 的继承实质上是先创建父类的实例对象 this(所以必须先调用父类的 `super()` 方法),然后再用子类的构造函数修改 this
  8. 3. ES5 的继承通过原型或构造函数机制来实现
  9. 4. ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象
  10. <a name="msfwW"></a>
  11. ## ES5 继承
  12. ```javascript
  13. function parent (a, b) {
  14. this.a = a;
  15. this.b = b;
  16. }
  17. function child(c) {
  18. this.c = c
  19. };
  20. // 通过子级去继承父级
  21. parent.call(child, 1, 2)

而去看 call 的底层方法可知,继承的过程是通过 prototype 属性

  1. child.prototype = new parent(1, 2);

由此可知, ES5 继承的实质是先创建了子类元素 child 的实例对象,然后再把父类元素 parent 的原型对象中的属性赋值给子类元素 child 的实例对象里面,从而实现继承。

ES6 中的继承

ES6 中的继承是基于 class 类之间继承的。通过关键字 extends 实现。
通过 super 实例化调用父类。

  1. class parent {
  2. constructor(a, b) {
  3. this.a = a;
  4. this.b = b;
  5. }
  6. parentMethods() {
  7. return this.a + this.b;
  8. }
  9. }
  10. class child extends parent {
  11. constructor(a, b, c) {
  12. super(a, b);
  13. this.c = c;
  14. }
  15. childMethods() {
  16. return this.c + ',' + super.parentMethods();
  17. }
  18. }
  19. const point = new child(1, 2, 3);
  20. alert(point.childMethods());

参考链接: