JavaScript常用八種繼承方案

木易楊說發表於2018-10-20

更新:在常用七種繼承方案的基礎之上增加了ES6的類繼承,所以現在變成八種啦。

--- 2018.10.30

1、原型鏈繼承

建構函式、原型和例項之間的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個原型物件的指標。

繼承的本質就是複製,即重寫原型物件,代之以一個新型別的例項

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
複製程式碼

JavaScript常用八種繼承方案

原型鏈方案存在的缺點:多個例項對引用型別的操作會被篡改。

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"
複製程式碼

2、借用建構函式繼承

使用父類的建構函式來增強子類例項,等同於複製父類的例項給子類(不使用原型)

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中的屬性複製一份。

缺點:

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

3、組合繼承

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

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
複製程式碼

JavaScript常用八種繼承方案

缺點:

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

例項物件instance1上的兩個屬性就遮蔽了其原型物件SubType.prototype的兩個同名屬性。所以,組合模式的缺點就是在使用子類建立例項物件時,其原型中會存在兩份相同的屬性/方法。

4、原型式繼承

利用一個空物件作為中介,將某個物件直接賦值給空物件建構函式的原型。

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方法。

5、寄生式繼承

核心:在原型式繼承的基礎上,增強物件,返回建構函式

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"
複製程式碼

缺點(同原型式繼承):

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

6、寄生組合式繼承

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

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"]
複製程式碼

JavaScript常用八種繼承方案

這個例子的高效率體現在它只呼叫了一次SuperType 建構函式,並且因此避免了在SubType.prototype 上建立不必要的、多餘的屬性。於此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof 和isPrototypeOf()

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

7、混入方式繼承多個物件

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 的方法。

8、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;
    }
}
複製程式碼

總結

1、函式宣告和類宣告的區別

函式宣告會提升,類宣告不會。首先需要宣告你的類,然後訪問它,否則像下面的程式碼會丟擲一個ReferenceError。

let p = new Rectangle(); 
// ReferenceError

class Rectangle {}
複製程式碼

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

  • ES5的繼承實質上是先建立子類的例項物件,然後再將父類的方法新增到this上(Parent.call(this)).

  • ES6的繼承有所不同,實質上是先建立父類的例項物件this,然後再用子類的建構函式修改this。因為子類沒有自己的this物件,所以必須先呼叫父類的super()方法,否則新建例項報錯。

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

交流

本人Github連結如下,歡迎各位Star

github.com/yygmind/blo…

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

JavaScript常用八種繼承方案

相關文章