JavaScript經常使用八種繼承方案

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看javascript

更新:在經常使用七種繼承方案的基礎之上增長了ES6的類繼承,因此如今變成八種啦。前端

------ 如下是正文 ------java

一、原型鏈繼承

構造函數、原型和實例之間的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個原型對象的指針。git

繼承的本質就是複製,即重寫原型對象,代之以一個新類型的實例github

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

// 這裏是關鍵,建立SuperType的實例,並將該實例賦值給SubType.prototype
SubType.prototype = new SuperType(); 

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true
複製代碼

原型鏈方案存在的缺點:多個實例對引用類型的操做會被篡改。面試

function SuperType(){
  this.colors = ["red", "blue", "green"];
}
function SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"
複製代碼

二、借用構造函數繼承

使用父類的構造函數來加強子類實例,等同於複製父類的實例給子類(不使用原型)segmentfault

function SuperType(){
    this.color=["red","green","blue"];
}
function SubType(){
    //繼承自SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"
複製代碼

核心代碼是SuperType.call(this),建立子類實例時調用SuperType構造函數,因而SubType的每一個實例都會將SuperType中的屬性複製一份。前端工程師

缺點:函數

  • 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
  • 沒法實現複用,每一個子類都有父類實例函數的副本,影響性能

三、組合繼承

組合上述兩種方法就是組合繼承。用原型鏈實現對原型屬性和方法的繼承,用借用構造函數技術來實現實例屬性的繼承。性能

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 繼承屬性
  // 第二次調用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 繼承方法
// 構建原型鏈
// 第一次調用SuperType()
SubType.prototype = new SuperType(); 
// 重寫SubType.prototype的constructor屬性,指向本身的構造函數SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
複製代碼

缺點:

  • 第一次調用SuperType():給SubType.prototype寫入兩個屬性name,color。
  • 第二次調用SuperType():給instance1寫入兩個屬性name,color。

實例對象instance1上的兩個屬性就屏蔽了其原型對象SubType.prototype的兩個同名屬性。因此,組合模式的缺點就是在使用子類建立實例對象時,其原型中會存在兩份相同的屬性/方法。

四、原型式繼承

利用一個空對象做爲中介,將某個對象直接賦值給空對象構造函數的原型。

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
複製代碼

object()對傳入其中的對象執行了一次淺複製,將構造函數F的原型直接指向傳入的對象。

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"
複製代碼

缺點:

  • 原型鏈繼承多個實例的引用類型屬性指向相同,存在篡改的可能。
  • 沒法傳遞參數

另外,ES5中存在Object.create()的方法,可以代替上面的object方法。

五、寄生式繼承

核心:在原型式繼承的基礎上,加強對象,返回構造函數

function createAnother(original){
  var clone = object(original); // 經過調用 object() 函數建立一個新對象
  clone.sayHi = function(){  // 以某種方式來加強對象
    alert("hi");
  };
  return clone; // 返回這個對象
}
複製代碼

函數的主要做用是爲構造函數新增屬性和方法,以加強函數

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
複製代碼

缺點(同原型式繼承):

  • 原型鏈繼承多個實例的引用類型屬性指向相同,存在篡改的可能。
  • 沒法傳遞參數

六、寄生組合式繼承

結合借用構造函數傳遞參數和寄生模式實現繼承

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 建立對象,建立父類原型的一個副本
  prototype.constructor = subType;                    // 加強對象,彌補因重寫原型而失去的默認的constructor 屬性
  subType.prototype = prototype;                      // 指定對象,將新建立的對象賦值給子類的原型
}

// 父類初始化實例屬性和原型屬性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用構造函數傳遞加強子類實例屬性(支持傳參和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 將父類原型指向子類
inheritPrototype(SubType, SuperType);

// 新增子類原型屬性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
複製代碼

這個例子的高效率體如今它只調用了一次SuperType 構造函數,而且所以避免了在SubType.prototype 上建立沒必要要的、多餘的屬性。於此同時,原型鏈還能保持不變;所以,還可以正常使用instanceof 和isPrototypeOf()

這是最成熟的方法,也是如今庫實現的方法

七、混入方式繼承多個對象

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 繼承一個類
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 從新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do something
};
複製代碼

Object.assign會把 OtherSuperClass原型上的函數拷貝到 MyClass原型上,使 MyClass 的全部實例均可用 OtherSuperClass 的方法。

八、ES6類繼承extends

extends關鍵字主要用於類聲明或者類表達式中,以建立一個類,該類是另外一個類的子類。其中constructor表示構造函數,一個類中只能有一個構造函數,有多個會報出SyntaxError錯誤,若是沒有顯式指定構造方法,則會添加默認的 constructor方法,使用例子以下。

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    
    // Getter
    get area() {
        return this.calcArea()
    }
    
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 輸出 200

-----------------------------------------------------------------
// 繼承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);
    
    // 若是子類中存在構造函數,則須要在使用「this」以前首先調用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 輸出 100
複製代碼

extends繼承的核心代碼以下,其實現和上述的寄生組合式繼承方式同樣

function _inherits(subType, superType) {
  
    // 建立對象,建立父類原型的一個副本
    // 加強對象,彌補因重寫原型而失去的默認的constructor 屬性
    // 指定對象,將新建立的對象賦值給子類的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}
複製代碼

總結

一、函數聲明和類聲明的區別

函數聲明會提高,類聲明不會。首先須要聲明你的類,而後訪問它,不然像下面的代碼會拋出一個ReferenceError。

let p = new Rectangle(); 
// ReferenceError

class Rectangle {}
複製代碼

二、ES5繼承和ES6繼承的區別

  • ES5的繼承實質上是先建立子類的實例對象,而後再將父類的方法添加到this上(Parent.call(this)).

  • ES6的繼承有所不一樣,實質上是先建立父類的實例對象this,而後再用子類的構造函數修改this。由於子類沒有本身的this對象,因此必須先調用父類的super()方法,不然新建實例報錯。

《javascript高級程序設計》筆記:繼承
MDN之Object.create()
MDN之Class

交流

本人Github連接以下,歡迎各位Star

github.com/yygmind/blo…

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

相關文章
相關標籤/搜索