說實在話,以前我只需要知道“寄生組合繼承”是最好的,有個祖傳程式碼模版用就行。最近因為一些事情,幾個星期以來一直心心念念想整理出來。本文以《JavaScript高階程式設計》上的內容為骨架,補充了ES6 Class的相關內容,從我認為更容易理解的角度將繼承這件事敘述出來,希望大家能有所收穫。
1. 繼承分類
先來個整體印象。如圖所示,JS中繼承可以按照是否使用object函式(在下文中會提到),將繼承分成兩部分(Object.create是ES5新增的方法,用來規範化這個函式)。
其中,原型鏈繼承和原型式繼承有一樣的優缺點,建構函式繼承與寄生式繼承也相互對應。寄生組合繼承基於Object.create, 同時優化了組合繼承,成為了完美的繼承方式。ES6 Class Extends的結果與寄生組合繼承基本一致,但是實現方案又略有不同。
下面馬上進入正題。
2. 繼承方式
上圖上半區的原型鏈繼承,建構函式繼承,組合繼承,網上內容比較多,本文不作詳細描述,只指出重點。這裡給出了我認為最容易理解的一篇《JS中的繼承(上)》。如果對上半區的內容不熟悉,可以先看這篇文章,再回來繼續閱讀;如果已經比較熟悉,這部分可以快速略過。另,上半區大量借用了yq前端的一篇繼承文章[1]。
2.1 原型式繼承
核心:將父類的例項作為子類的原型
1 2 3 |
SubType.prototype = new SuperType() // 所有涉及到原型鏈繼承的繼承方式都要修改子類建構函式的指向,否則子類例項的建構函式會指向SuperType。 SubType.prototype.constructor = SubType; |
優點:父類方法可以複用
缺點:
- 父類的引用屬性會被所有子類例項共享
- 子類構建例項時不能向父類傳遞引數
2.2 建構函式繼承
核心:將父類建構函式的內容複製給了子類的建構函式。這是所有繼承中唯一一個不涉及到prototype的繼承。
1 |
SuperType.call(SubType); |
優點:和原型鏈繼承完全反過來。
- 父類的引用屬性不會被共享
- 子類構建例項時可以向父類傳遞引數
缺點:父類的方法不能複用,子類例項的方法每次都是單獨建立的。
2.3 組合繼承
核心:原型式繼承和建構函式繼承的組合,兼具了二者的優點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SuperType() { this.name = 'parent'; this.arr = [1, 2, 3]; } SuperType.prototype.say = function() { console.log('this is parent') } function SubType() { SuperType.call(this) // 第二次呼叫SuperType } SubType.prototype = new SuperType() // 第一次呼叫SuperType |
優點:
- 父類的方法可以被複用
- 父類的引用屬性不會被共享
- 子類構建例項時可以向父類傳遞引數
缺點:
呼叫了兩次父類的建構函式,第一次給子類的原型新增了父類的name, arr屬性,第二次又給子類的建構函式新增了父類的name, arr屬性,從而覆蓋了子類原型中的同名引數。這種被覆蓋的情況造成了效能上的浪費。
2.4 原型式繼承
核心:原型式繼承的object方法本質上是對引數物件的一個淺複製。
優點:父類方法可以複用
缺點:
- 父類的引用屬性會被所有子類例項共享
- 子類構建例項時不能向父類傳遞引數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function object(o){ function F(){} F.prototype = o; return new 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" |
ECMAScript 5 通過新增 Object.create()方法規範化了原型式繼承。這個方法接收兩個引數:一 個用作新物件原型的物件和(可選的)一個為新物件定義額外屬性的物件。在傳入一個引數的情況下, Object.create()與 object()方法的行為相同。——《JAVASCript高階程式設計》
所以上文中程式碼可以轉變為
1 |
var yetAnotherPerson = object(person); => var yetAnotherPerson = Object.create(person); |
2.5 寄生式繼承
核心:使用原型式繼承獲得一個目標物件的淺複製,然後增強這個淺複製的能力。
優缺點:僅提供一種思路,沒什麼優點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function createAnother(original){ var clone=object(original); //通過呼叫函式建立一個新物件 clone.sayHi = function(){ //以某種方式來增強這個物件 alert("hi"); }; return clone; //返回這個物件 } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi" |
2.6 寄生組合繼承
剛才說到組合繼承有一個會兩次呼叫父類的建構函式造成浪費的缺點,寄生組合繼承就可以解決這個問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); // 建立了父類原型的淺複製 prototype.constructor = subType; // 修正原型的建構函式 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); } |
優缺點:這是一種完美的繼承方式。
2.7 ES6 Class extends
核心: ES6繼承的結果和寄生組合繼承相似,本質上,ES6繼承是一種語法糖。但是,寄生組合繼承是先建立子類例項this物件,然後再對其增強;而ES6先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。
1 2 3 4 5 6 7 |
class A {} class B extends A { constructor() { super(); } } |
ES6實現繼承的具體原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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中不是。
3. 總結
- ES6 Class extends是ES5繼承的語法糖
- JS的繼承除了建構函式繼承之外都基於原型鏈構建的
- 可以用寄生組合繼承實現ES6 Class extends,但是還是會有細微的差別
參考文章:
[1]《js繼承、建構函式繼承、原型鏈繼承、組合繼承、組合繼承優化、寄生組合繼承》
[2]《JavaScript高階程式設計》