衆所周知,JavaScript 中,沒有 JAVA 等主流語言「類」的概念,更沒有「父子類繼承」的概念,而是經過原型對象和原型鏈的方式實現繼承。javascript
因而,咱們這一篇講一講 JS 中的繼承(委託)。java
JavaScript 是面向對象編程的語言,裏面全是對象,而若是不經過繼承的機制將對象聯繫起來,勢必會形成程序代碼的冗餘,不方便書寫。編程
好,既然是 OO 語言,那麼就加繼承屬性吧。可是 JS 創造者並不打算引用 class,否則 JS 就是一個完整的 OOP 語言了,而創造者 JS 更容易讓新手開發。app
後來,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 畸形的繼承方式:原型鏈繼承~this
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
以上例子中,暴露出原型鏈繼承的兩個問題:spa
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 的原理是在子類型的構造函數中,「借調」父類型的構造函數,最終實現子類型中擁有父類型中屬性的副本了。prototype
call 或 apply 這種繼承方式在《JavaScript 高級程序設計》中叫做「借用構造函數(constructor stealing)」,解決了原型鏈繼承中,引用數據類型被全部子實例共享的問題,也可以實現傳遞參數到構造函數中,但惟一的問題在於業務代碼也寫在了構造函數中,函數得不到複用。設計
組合繼承(combination inheritance)也叫做僞經典繼承,指的是,前面兩種方法:原型鏈繼承和 call 或 apply 繼承 組合起來,保證了實例都有本身的屬性,同時也可以實現函數複用:code
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 最經常使用的繼承方式。
原型式繼承說白了,就是將父類型做爲一個對象,直接變成子類型的原型對象。
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() 方法。
寄生式繼承是把原型式 + 工廠模式結合起來,目的是爲了封裝建立的過程。
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"
剛纔說到組合繼承有一個會兩次調用父類的構造函數形成浪費的缺點,寄生組合繼承就能夠解決這個問題。
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"]
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
歡迎關注個人我的公衆號「謝南波」,專一分享原創文章。