JS專題之繼承

前言

衆所周知,JavaScript 中,沒有 JAVA 等主流語言「類」的概念,更沒有「父子類繼承」的概念,而是經過原型對象和原型鏈的方式實現繼承。javascript

因而,咱們這一篇講一講 JS 中的繼承(委託)。java

1、爲何要有繼承?

JavaScript 是面向對象編程的語言,裏面全是對象,而若是不經過繼承的機制將對象聯繫起來,勢必會形成程序代碼的冗餘,不方便書寫。編程

2、爲何又是原型鏈繼承?

好,既然是 OO 語言,那麼就加繼承屬性吧。可是 JS 創造者並不打算引用 class,否則 JS 就是一個完整的 OOP 語言了,而創造者 JS 更容易讓新手開發。數組

後來,JS 創造者就將 new 關鍵字建立對象後面不接 class,改爲構造函數,又考慮到繼承,因而在構造函數上加一個原型對象,最後讓全部經過new 構造函數 建立出來的對象,就繼承構造函函數的原型對象的屬性。閉包

function Person() {
    // 構造函數
    this.name = "jay";
}

Person.prototype = {
    sex: "male"
}

var person1 = new Person();
console.log(person1.name);  // jay
console.log(person1.sex);  // male
複製代碼

因此,就有了 JavaScript 畸形的繼承方式:原型鏈繼承~app

3、原型鏈繼承

function Parent() {
    this.names = ["aa", "bb", "cc"];
    this.age = 18;
}

function Child() {
    // ...
}

Child.prototype = new Parent();  // 改變構造函數的原型對象

var child1 = new Child();

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
console.log(child1.age);   // 18
child1.names.push("dd");
child1.age = 20;
var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age);  // 18
複製代碼

以上例子中,暴露出原型鏈繼承的兩個問題:函數

  1. 包含引用類型數據的原型屬性,會被全部實例共享,基本數據類型則不會。
  2. 在建立子類型實例時,沒法向父類型的構造函數中傳遞參數。

4、call 或 apply 繼承

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}
function Child() {
    Parent.call(this, 18);
}

var child1 = new Child();

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age);  // 18
複製代碼

call 或 apply 的原理是在子類型的構造函數中,「借調」父類型的構造函數,最終實現子類型中擁有父類型中屬性的副本了。post

call 或 apply 這種繼承方式在《JavaScript 高級程序設計》中叫做「借用構造函數(constructor stealing)」,解決了原型鏈繼承中,引用數據類型被全部子實例共享的問題,也可以實現傳遞參數到構造函數中,但惟一的問題在於業務代碼也寫在了構造函數中,函數得不到複用。ui

5、組合繼承

組合繼承(combination inheritance)也叫做僞經典繼承,指的是,前面兩種方法:原型鏈繼承和 call 或 apply 繼承 組合起來,保證了實例都有本身的屬性,同時也可以實現函數複用:this

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}

Parent.prototype.sayName = function () {
    console.log(this.names);
}

function Child() {
    Parent.call(this, 18);  // 第一次調用
}

Child.prototype = new Parent();  // 第二次調用:經過原型鏈繼承 sayName 方法
Child.prototype.constructor = Child;  // 改變 constructor 爲子類型構造函數

var child1 = new Child();
child1.sayName();   // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age); 
child2.sayName();  // ["aa", "bb", "cc"]
複製代碼

組合繼承將繼承分爲兩步,一次是建立子類型關聯父類型原型對象的時候,另外一次是在子類型構造函數的內部。是 JS 最經常使用的繼承方式。

6、原型式繼承

原型式繼承說白了,就是將父類型做爲一個對象,直接變成子類型的原型對象。

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
 
var parent = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 

var child1 = object(parent);

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = object(parent);
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18
複製代碼

原型式繼承其實就是對原型鏈繼承的一種封裝,它要求你有一個已有的對象做爲基礎,可是原型式繼承也有共享父類引用屬性,沒法傳遞參數的缺點。

這個方法後來有了正式的 API: Object.create({...})

因此當有一個對象,想讓子實例繼承的時候,能夠直接用 Object.create() 方法。

7、寄生式繼承

寄生式繼承是把原型式 + 工廠模式結合起來,目的是爲了封裝建立的過程。

function createAnother(original){ 
    var clone= object(original);    //經過調用函數建立一個新對象
    clone.sayHi = function(){      //以某種方式來加強這個對象
        console.log("hi");
    };
    return clone;                  //返回這個對象
}
 
var person = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
複製代碼

8、 寄生組合式繼承

剛纔說到組合繼承有一個會兩次調用父類的構造函數形成浪費的缺點,寄生組合繼承就能夠解決這個問題。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); // 建立了父類原型的淺複製
    prototype.constructor = subType;             // 修正原型的構造函數
    subType.prototype = prototype;               // 將子類的原型替換爲這個原型
}
 
function SuperType(age){
    this.age = age;
    this.names = ["aa", "bb", "cc"];
}
 
SuperType.prototype.sayName = function(){
    console.log(this.names);
};
 
function SubType(age){
    SuperType.call(this, age);
    this.age = age;
}
// 核心:由於是對父類原型的複製,因此不包含父類的構造函數,也就不會調用兩次父類的構造函數形成浪費
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var child1 = new SubType(22)
child1.sayAge()  // 22
child1.sayName()  // ["aa", "bb", "cc"]
複製代碼

9、ES6 class extends

class Parent {
    constructor(name) {
	this.name = name;
    }
    doSomething() {
	        console.log('parent do something!');
    }
    sayName() {
	    console.log('parent name:', this.name);
    }
}

class Child extends Parent {
    constructor(name, parentName) {
	super(parentName);
	this.name = name;
    }
    sayName() {
 	    console.log('child name:', this.name);
    }
}

const child = new Child('son', 'father');
child.sayName();            // child name: son
child.doSomething();        // parent do something!

const parent = new Parent('father');
parent.sayName();   // parent name: father
複製代碼

ES6 的 class extends 本質上是 ES5 的語法糖。 ES6實現繼承的具體原理:

class Parent {
}
 
class Child {
}
 
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
 
// B 的實例繼承 A 的實例
Object.setPrototypeOf(Child.prototype, parent.prototype);
 
// B 繼承 A 的靜態屬性
Object.setPrototypeOf(Child, Parent);
複製代碼

總結

javascript 因爲歷史發展緣由,繼承方式其實是經過原型鏈屬性查找的方式,但正規的叫法不叫繼承而叫「委託」,ES6 的 class extends 關鍵字也不過是 ES5 的語法糖。因此,瞭解 JS 的原型和原型鏈很是重要,詳情請翻看我以前的文章《JavaScript原型與原型鏈》

參考:
《JavaScript 高級程序設計》

2019/02/10 @Starbucks

歡迎關注個人我的公衆號「謝南波」,專一分享原創文章。

掘金專欄 JavaScript 系列文章

  1. JavaScript之變量及做用域
  2. JavaScript之聲明提高
  3. JavaScript之執行上下文
  4. JavaScript之變量對象
  5. JavaScript之原型與原型鏈
  6. JavaScript之做用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中完全理解this
  12. JavaScript專題之模擬實現call和apply
  13. JavaScript專題之模擬實現bind
  14. JavaScript專題之模擬實現new
  15. JS專題之事件模型
  16. JS專題之事件循環
  17. JS專題之去抖函數
  18. JS專題之節流函數
  19. JS專題之函數柯里化
  20. JS專題之數組去重
  21. JS專題之深淺拷貝
  22. JS專題之數組展開
  23. JS專題之嚴格模式
  24. JS專題之memoization
  25. JS專題之垃圾回收
相關文章
相關標籤/搜索