對於科班出身的同窗來說,絕大多數應該是從 過程化編程
起步,這種風格的代碼之包含了過程(函數)調用,沒有對基層進行抽象(麪條式代碼)。javascript
後來咱們開始接觸到面向對象編程,進而又跟另一個被稱爲 類
的術語扯上了關係,或者能夠說是面向類式編程。前端
在後來隨着編程的深刻咱們開始接觸到 函數式編程
,這也是一種編程的選擇或者習慣。java
但咱們這次只討論一下關於類的那些事兒。說到類,咱們便會想起三大基本特性 封裝
、繼承
、多態
,而咱們這次的主題即是 繼承
。編程
相較於傳統語言,JavaScript 中一直在模仿類行爲。直到 ES6 版本出來後纔出現了一些近似類的功能,如 class
、extends
、super
。可是這並不表明 JavaScript 實現了像傳統語言同樣的類,JavaScript 的核心機制是[[prototype]]
,而且只有對象,對象只負責定義自身的行爲。像這些新定義的語法只是在原型鏈的基礎上進行的封裝(語法糖),所以搞懂[[prototype]]
纔是關鍵。app
說了一些體外話,而後進入主題。咱們先來拋出兩個小問題:函數式編程
接下來咱們一個個的講。函數
先看一下例子:性能
function Animal() { this.categories = ['二哈', '英短', '龍貓']; } Animal.prototype.category = function() { console.log(this.categories); } var animal = function () {} animal.prototype = new Animal(); var animal1 = new animal(); animal1.category(); // ['二哈', '英短', '龍貓'];
這是最基本的類式繼承,經過使用父級的構造函數調用來爲 animal.prototype
進行關聯。咱們先來講一下使用構造函數調用(new)時會自動執行的一些狀況:this
[[Prototype]]
關聯,也就是說這個對新象會關聯到animal.prototype
對象上。this
,也就是說此時的this
指的是 animal
。new
關鍵字調用函數後會返回這個新對象(也就是 animal{}
)。關於this
更細緻的討論能夠參見js之thisprototype
因此當咱們執行 animal1.category()
操做的時候,由於 [[Get]]
操做的默認行爲會檢查原型鏈,animal1
自身沒有 categories
屬性因此會到自身原型鏈查找,因爲new Animal()
操做返回的對象與Animal.prototype
自動關聯而且animal.prototype
還保存着 Animal.prototype
引用,所以animal1
即可以順利的訪問到Animal
原型鏈及自身的屬性。
咱們再來看一下例子:
function Animal() { this.categories = ['二哈', '英短', '龍貓']; } var animal = function () {} animal.prototype = new Animal(); var animal1 = new animal(); var animal2 = new animal(); console.log( animal1.categories); // ["二哈", "英短", "龍貓"] animal1.categories.push('柯基'); console.log( animal2.categories); // ["二哈", "英短", "龍貓", '柯基']
經過這個例子就能夠很明顯的看出使用類式繼承的問題:
this
添加引用類型對象,當這個對象被更改時,全部子級都會受到牽連。針對於這些問題咱們引出了另一種繼承。
function Animal(name) { this.name = name; this.features = ['裝傻賣萌', '好吃懶作']; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡覺'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } var dog1 = new Dog('二哈', '汪汪。。'); var dog2 = new Dog('柯基', '汪汪。。'); dog1.features.push('拆家小分隊'); console.log(dog1.features); // ["裝傻賣萌", "好吃懶作", "拆家小分隊"] console.log(dog2.features); // ["裝傻賣萌", "好吃懶作"] console.log(dog1.sleep()); // TypeError: sleep is not a function
前面咱們有講到經過構造函數調用的時候發生的狀況,因爲未執行原型鏈的關聯,因此當執行完構造函數調用以後自動將 this
關聯到 Dog
併爲其添加屬性。這段代碼的核心是 Animal.call(this, name)
,這裏經過顯示綁定將 Animal
中的屬性從新添加到 Dog
對象中。
提醒:Animal.call
和Animal.apply
用法相同,都會更改當前執行上下文環境的this,這種方式稱爲this
顯示綁定。還有一種被稱爲應綁定的方法:bind
,一樣會更改執行上下文環境的this,但bind
會返回執行函數的一個副本。
那既然這兩種都不能實現一個完整的繼承過程,咱們能夠結合一下這兩種思想,使用構造函數將父級的公有屬性與子級的公有屬性進行合併,同時要將父級原型鏈上屬性也進行合併(注意子級自已的公有屬性要後執行)。注意:不要直接執行父級的構造函數調用,由於使用 call
已經執行了調用了構造函數。再使用 new
操做至關於執行了兩遍重複的操做。
最先提出的這一方式的是美國的道格拉斯·克羅克福德(Douglas Crockford),世界著名的前端大師,同時也是JSON
的創立者。他提出的這個方案:
function inheritObject(proto) { function F() {}; F.prototype = proto; return new F(); }
這段代碼使用了一個一次性函數,經過改寫它的 .prototy
將它指向想要關聯的對象,而後再使用 new
操做構造一個新對象進行關聯。
function Animal(name) { this.name = name; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡覺。'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } Dog.prototype = inheritObject(Animal.prototype); Dog.prototype.yell = function() { console.log(this.name + ': ' + this.voice); } var dog = new Dog('二哈', '汪汪。。'); dog.sleep(); // 二哈正在睡覺。 dog.yell(); // 二哈: 汪汪。。
須要注意一點:通過 inheritObject
後已經沒有 Dog.prototype.constructor
屬性了,由於Dog.prototype
指向的是 Animal.prototype
,因此若是還須要這個屬性,須要手動修復它:
Dog.prototype.constructor = Dog
所以便出現了更加理想的繼承方式:
function inheritPrototype(subClass, superClass) { var f = inheritObject(superClass.prototype); f.constructor = subClass; subClass.prototype = f; } function Animal(name) { this.name = name; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡覺。'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } // 不考慮 construstor 指向的時候: // Dog.prototype = inheritObject(Animal.prototype); // 或者 Dog.prototype = Object.create(Animal.prototype) // 考慮 construstor 指向的時候: // 使用Object.create後手動修復construstor: Dog.prototype.constructor = Animal inheritPrototype(Dog, Animal); // 或者 Object.setPrototypeOf(Dog.prototype, Animal.prototype) Dog.prototype.yell = function() { console.log(this.name + '餓了: ' + this.voice); } var dog = new Dog('二哈', '汪汪。。'); dog.sleep(); // 二哈正在睡覺。 dog.yell(); // 二哈餓了: 汪汪。。
隨着這種方式的深刻,後來ES5便出現了 Object.create
這個方法,固然這個方法內部還有不少附加功能,可是核心倒是如此。可是這樣會致使 constructor
指向錯誤,進而咱們引出了 inheritPrototype()
方法修復其 constructor
指向的問題。一樣,ES6以後出現了Object.setPrototypeOf(subProto, superProto)
,這個方法實際上跟咱們本身寫的 inheritPrototype()
是相似的。
若是不考慮constructor
指向錯誤問題及輕微性能損失(被丟棄F對象會在適當時機被GC回收掉),使用Object.create
是徹底沒問題的。此外,
Obeject.create
會建立一個擁有空原型連的對象,這個對象沒有原型鏈,沒法進行進行委託。這種特殊的空對象特別適合作爲字典
結構來存儲數據。所以,該對象沒法使用instanceof
關鍵字,而且在使用for..in
遍歷對象的時候,使用Object.prototype.hasOwnProperty.call
來避免類型錯誤。
對於多繼承來說,咱們能夠換個思路。咱們剛剛將到,單一方式最完善的繼承是寄生組合式,其實多繼承徹底能夠照這個思路將多個類的公有屬性經過 call
或着 apply
的從新綁定功能將屬性拷貝到自身,而對於原型鏈上的屬性,則可使用原型繼承(須要將其它類的原型進行混入
)。
function SuperClass() { this.name = "SuperClass" } SuperClass.prototype.superMethod = function () { // .. } function OtherSuperClass() { this.otherName = "OtherSuperClass" } OtherSuperClass.prototype.otherSuperMethod = function () { // .. } function MyClass() { SuperClass.call(this); OtherSuperClass.call(this); } // 混入原型對象 Object.setPrototypeOf(SuperClass.prototype, OtherSuperClass.prototype) Object.setPrototypeOf(MyClass.prototype, SuperClass.prototype) MyClass.prototype.myMethod = function() { console.log('myMethod') }; var myClass = new MyClass(); console.log(myClass); // MyClass {name: "SuperClass", otherName: "OtherSuperClass"}