js之類繼承

前言

對於科班出身的同窗來說,絕大多數應該是從 過程化編程 起步,這種風格的代碼之包含了過程(函數)調用,沒有對基層進行抽象(麪條式代碼)。javascript

後來咱們開始接觸到面向對象編程,進而又跟另一個被稱爲 的術語扯上了關係,或者能夠說是面向類式編程。前端

在後來隨着編程的深刻咱們開始接觸到 函數式編程,這也是一種編程的選擇或者習慣。java

但咱們這次只討論一下關於類的那些事兒。說到類,咱們便會想起三大基本特性 封裝繼承多態,而咱們這次的主題即是 繼承編程

JavaScript 中的 「類」

相較於傳統語言,JavaScript 中一直在模仿類行爲。直到 ES6 版本出來後纔出現了一些近似類的功能,如 classextendssuper。可是這並不表明 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

  1. 建立(構造)一個新對象。
  2. 這個新對象會被執行[[Prototype]]關聯,也就是說這個對新象會關聯到animal.prototype對象上。
  3. 這個新對象會綁定到函數調用的this,也就是說此時的this指的是 animal
  4. 若是函數未返回其它對象,那麼使用 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); // ["二哈", "英短", "龍貓", '柯基']

經過這個例子就能夠很明顯的看出使用類式繼承的問題:

  • 若是父級的構造函數(使用new調用)裏存在經過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.callAnimal.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"}
相關文章
相關標籤/搜索