JS進階系列 --- 繼承

帕尼尼0_0發表於2018-08-05

轉載自:一篇文章理解JS繼承——原型鏈/建構函式/組合/原型式/寄生式/寄生組合/Class extends
同時加入了我個人的一些例子和淺見

繼承分類

先來個整體印象。如圖所示,JS中繼承可以按照是否使用object函式(在下文中會提到),將繼承分成兩部分(Object.create是ES5新增的方法,用來規範化這個函式)。

其中,原型鏈繼承和原型式繼承有一樣的優缺點,建構函式繼承與寄生式繼承也相互對應。寄生組合繼承基於Object.create, 同時優化了組合繼承,成為了完美的繼承方式。ES6 Class Extends的結果與寄生組合繼承基本一致,但是實現方案又略有不同。

這裡寫圖片描述

繼承方式

1. 原型鏈繼承
SubType.prototype = new SuperType() 
/*所有涉及到原型鏈繼承的繼承方式都要修改子類建構函式的指向,否則子類例項的建構函式會指向SuperType*/
SubType.prototype.constructor = SubType;

核心:將父類的例項作為子類的原型。

優點:父類方法可以複用。

缺點

  • 父類的引用屬性會被所有子類例項共享
  • 子類構建例項時不能向父類傳遞引數

例子

function Animal(){};
Animal.prototype.say = function(){console.log("動物叫聲!")}
function Dog(){};
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {console.log("汪汪汪!");}

var wc = new Dog();

記憶體圖分析

這裡寫圖片描述

2. 建構函式繼承

核心:將父類建構函式的內容複製給了子類的建構函式。這是所有繼承中唯一一個不涉及到prototype的繼承。

SuperType.call(SubType);

優點:和原型鏈繼承完全反過來

  • 父類的引用屬性不會被共享
  • 子類構建例項時可以向父類傳遞引數

缺點:父類的方法不能複用,子類例項的方法每次都是單獨建立的。

例子

function Animal(name){this.name = name};
Animal.prototype.say = function(){console.log("動物叫聲!")}
function Dog(name,age){Animal.call(this,name);this.age=age;};
Dog.prototype.bark = function () {console.log("汪汪汪!");}

var wc = new Dog("旺財",3);

記憶體圖分析
這裡寫圖片描述

3. 組合繼承

核心:原型鏈繼承和建構函式繼承的組合,兼具了二者的優點。

優點

  • 父類的方法可以被複用
  • 父類的引用屬性不會被共享
  • 子類構建例項時可以向父類傳遞引數

缺點:呼叫了兩次父類的建構函式,第一次給子類的原型新增了父類的屬性,第二次又給子類的建構函式新增了父類的屬性,從而覆蓋了子類原型中的同名引數。這種被覆蓋的情況造成了效能上的浪費。

例子

function Animal(name){this.name=name};
Animal.prototype.say = function(){console.log("動物叫聲!")}
function Dog(name,age){Animal.call(this,name);this.age=age;}; //這裡把name複製了一份給this
Dog.prototype = new Animal(); //這裡把name複製了一份給Dog.prototype
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {console.log("汪汪汪!");}

var wc = new Dog("旺財",3);

記憶體圖分析
這裡寫圖片描述

4. 原型式繼承

核心:原型式繼承的object方法本質上是對引數物件的一個淺複製。

優點:父類方法可以複用。

缺點

  • 父類的引用屬性會被所有子類例項共享
  • 子類構建例項時不能向父類傳遞引數

例子

function createAnimal(o) {
    function Animal(){}
    Animal.prototype = o;
    return new Animal();
}

var animal = {
    name: "動物",
    kinds: ["貓","狗","雞"]
};

var dog = createAnimal(animal);
var cat = createAnimal(animal);

記憶體圖分析
這裡寫圖片描述

ECMAScript 5 通過新增 Object.create()方法規範化了原型式繼承。這個方法接收兩個引數:一個用作新物件原型的物件;一個(可選的)為新物件定義額外屬性的物件。在傳入一個引數的情況下, Object.create()object()方法的行為相同。——《JAVASCript高階程式設計》

所以上文中程式碼可以轉變為:

var dog = object(animal); => var dog = Object.create(animal);

5. 寄生式繼承

核心:使用原型式繼承獲得一個目標物件的淺複製,然後增強這個淺複製的能力。

優缺點:僅提供一種思路,沒什麼優點。

例子:dog&cat 寄生式繼承 animal

function createAnimal(o) {
    var clone = Object.create(o);
    clone.say = function(){
        console.log("動物叫聲");
    }
    return clone;
}

var animal = {
    name: "動物",
    kinds: ["貓","狗","雞"]
};

var dog = createAnimal(animal);
var cat = createAnimal(animal);

記憶體圖分析
這裡寫圖片描述

6. 寄生組合繼承

核心:組合繼承有一個會兩次呼叫父類的建構函式造成浪費的缺點,寄生組合繼承就可以解決這個問題。

原理

因為組合繼承是中 Dog.prototype = new Animal,Dog的原型經過了一次Animal的new操作
而寄生組合繼承中 var prototype = Object.create(Animal.prototype); 在create函式內部,雖然做了一次new操作,但不是Animal的new操作,只是把new出來物件的prototype指向了Animal.prototype,並把這個物件的地址賦值給了prototype

優缺點:這是一種完美的繼承方式。
例子

function createAnimal(Obj,Animal) {
    var prototype = Object.create(Animal.prototype);
    prototype.constructor = Obj;
    Obj.prototype = prototype;
}
function Animal(name){this.name = name;this.colors = ["red","blue"];}
Animal.prototype.say = function(){console.log("動物叫聲!")}
function Dog(name, age){Animal.call(this, name);this.age = age;}
createAnimal(Dog, Animal);
Dog.prototype.bark = function(){console.log("汪汪汪!")};

var dog = new Dog("小黃",3);

記憶體圖分析

這裡寫圖片描述

7. ES6 Class extends

核心: ES6繼承的結果和寄生組合繼承相似,本質上,ES6繼承是一種語法糖。但是,寄生組合繼承是先建立子類例項this物件,然後再對其增強;而ES6先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

原理

class A {
}

class B {
}

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

// B 的例項繼承 A 的例項
Object.setPrototypeOf(B.prototype, A.prototype);

// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);

ES6繼承與ES5繼承的異同

相同點:本質上ES6繼承是ES5繼承的語法糖
不同點:

  • ES6繼承中子類的建構函式的原型鏈指向父類的建構函式,ES5中使用的是建構函式複製,沒有原型鏈指向。
  • ES6子類例項的構建,基於父類例項,ES5中不是。

總結

  1. ES6 Class extends是ES5繼承的語法糖
  2. JS的繼承除了建構函式繼承之外都基於原型鏈構建的
  3. 可以用寄生組合繼承實現ES6 Class extends,但是還是會有細微的差別

相關文章