關於JavaScript繼承的那些事

在JavaScript中,對象的建立能夠脫離類型(class free),經過字面量的方式能夠很方便的建立出自定義對象。函數

另外,JavaScript中擁有原型這個強大的概念,當對象進行屬性查找的時候,若是對象自己內找不到對應的屬性,就會去搜索原型鏈。因此,結合原型和原型鏈的這個特性,JavaScript就能夠用來實現對象之間的繼承了。this

下面就介紹一下JavaScript中的一些經常使用的繼承方式。spa

原型鏈繼承

因爲原型鏈搜索的這個特性,在JavaScript中能夠很方便的經過原型鏈來實現對象之間的繼承。prototype

下面看一個例子:code

function Person(name, age){
    this.name = name;
    this.age = age;
}    

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old!");
}


function Teacher(staffId){
    this.staffId = staffId;
}

Teacher.prototype = new Person();

var will = new Teacher(1000);
will.name = "Will";
will.age = 28;
will.getInfo();
// Will is 28 years old!

console.log(will instanceof Object);
// true
console.log(will instanceof Person);
// true
console.log(will instanceof Teacher);
// true

console.log(Object.prototype.isPrototypeOf(will));   
// true
console.log(Person.prototype.isPrototypeOf(will));
// true
console.log(Teacher.prototype.isPrototypeOf(will));
// true

在這個例子中,有兩個構造函數"Person"和"Teacher",經過"Teacher.prototype = new Person()"語句建立了一個"Person"對象,而且設置爲"Teacher"的原型。對象

經過這種方式,就實現了"Teacher"繼承"Person","will"這個對象能夠成功的調用"getInfo"這個屬於"Person"的方法。blog

在這個例子中,還演示了經過"instanceof"操做符和"isPrototypeOf()"方法來查看對象和原型之間的關係。繼承

對於原型鏈繼承,下面看看其中的一些細節問題。ip

 

constructor屬性

對於全部的JavaScript原型對象,都有一個"constructor"屬性,該屬性對應用來建立對象的構造函數。ci

對於"constructor"這個屬性,最大的做用就是能夠幫咱們標明一個對象的"類型"

在JavaScript中,當經過"typeof"查看Array對象的時候,返回的結果是"object"。這個咱們的預期結果,因此若是要判對一個對象究竟是不是Array類型,就能夠結合"constructor"屬性獲得想要的結果。

function isArray(myArray) {
    return myArray.constructor.toString().indexOf("Array") > -1;
}

var arr = []
console.log(typeof arr);
// object
console.log(isArray(arr));
// true

如今回到前面的例子,查看一下對象"will"的原型和構造函數:

從這個結果能夠看到,"will"的原型是"Person {name: undefined, age: undefined}"(經過new Person()構造出來的對象),"will"的構造函數是"function Person"。

等等,"will"不是經過"Teacher"建立出來的對象麼?爲何構造函數對於的是"function Person",而不是"function Teacher"?

下面,根據前面的例子繪製一張對象關係圖,從而分析一下繼承關係以及"constructor"屬性:

圖中給出了各類對象之間的關係,有幾點須要注意的是:

  • "Teacher.prototype"這個原型對象是經過"Person"構造函數建立出來的一個對象"Person {name: undefined, age: undefined}"
  • 對象"will"建立了本身的"name"和"age"屬性,並無使用父類對象的,而是覆蓋了父類的"name"和"age"屬性
  • 經過"will"訪問"constructor"這個屬性的時候,先找到了"Teacher.prototype"這個對象,而後找到"Person.prototype", 經過原型鏈查找訪問到了"constructor"屬性對應的"function Person"

     

重設constructor

爲了解決上面的問題,讓子類對象的"constructor"屬性對應正確的構造函數,咱們能夠重設子類原型對象的"constructor"屬性。

通常來講,能夠簡單的使用下面代碼來重設"constructor"屬性:

Teacher.prototype.constructor = Teacher;

可是經過這種方式重設"constructor"屬性會致使它的[[Enumerable]]特性被設置爲 true。默認狀況下,原生的"constructor"屬性是不可枚舉的。

所以若是使用兼容 ECMAScript 5 的 JavaScript 引擎,就可使用"Object.defineProperty()":

Object.defineProperty(Teacher.prototype, "constructor", {
    enumerable: false,
    value: Teacher
});

經過下面的結果能夠看到:

經過這個設置,對象"will" 的"constructor"屬性就指向了正確的"function Teacher"。

這時的對象關係圖就變成了以下,跟前面的關係圖比較,惟一的區別就是"Teacher.prototype"對象多了一個"constructor"屬性,而且這個屬性指向"function Teacher":

 

原型的動態性

原型對象是能夠修改的,因此,當建立了繼承關係以後,咱們能夠經過更新子類的原型對象給子類添加特有的方法。

例如經過下面的方式就給子類添加了一個特有的"getId"方法。

Teacher.prototype.getId = function(){
    console.log(this.name + "'s staff Id is " + this.staffId);
}

will.getId();
// Will's staff Id is 1000

可是,必定要區分原型的修改和原型的重寫。若是對原型進行了重寫,就會產生徹底不一樣的效果。

下面看看若是對"Teacher"的原型重寫會產生什麼效果,爲了分清跟前面代碼的順序,這裏貼出了完整的代碼:

function Person(name, age){
    this.name = name;
    this.age = age;
}    

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old!");
}


function Teacher(staffId){
    this.staffId = staffId;
}

Teacher.prototype = new Person();
Object.defineProperty(Teacher.prototype, "constructor", {
    enumerable: false,
    value: Teacher
});

var will = new Teacher(1000);
will.name = "Will";
will.age = 28;

// 更新原型 Teacher.prototype.getId
= function(){ console.log(this.name + "'s staff Id is " + this.staffId); } will.getId(); // Will's staff Id is 1000
// 重寫原型 Teacher.prototype = { getStaffId: function(){ console.log(this.name + "'s staff Id is " + this.staffId); } } will.getInfo(); // Will is 28 years old! will.getId(); // Will's staff Id is 1000 console.log(will.__proto__); // Person {name: undefined, age: undefined} console.log(will.__proto__.constructor); // function Teacher var wilber = new Teacher(1001); wilber.name = "Wilber"; wilber.age = 28; // wilber.getInfo(); // Uncaught TypeError: wilber.getInfo is not a function(…) wilber.getStaffId(); // Wilber's staff Id is 1001 console.log(wilber.__proto__); // Object {} console.log(wilber.__proto__.constructor); // function Object() { [native code] }

通過重寫原型以後狀況更加複雜了,下面就來看看重寫原型以後的對象關係圖:

從關係圖能夠看到:

  • 原型對象能夠被更新,經過"Teacher.prototype.getId"給"will"對象的原型添加了"getId"方法
  • 重寫原型以後,在重寫原型以前建立的對象的"[[prototype]]"屬性依然指向原來的原型對象;在重寫原型以後建立的對象的"[[prototype]]"屬性將指向新的原型對象
  • 對於重寫原型先後建立的兩種對象,對象的屬性查找將搜索不一樣的原型鏈

組合繼承

在經過原型鏈方式實現的繼承中,父類和子類的構造函數相對獨立,若是子類構造函數能夠調用父類的構造函數,而且進行相關的初始化,那就比較好了。

這時就想到了JavaScript中的call方法,經過這個方法能夠動態的設置this的指向,這樣就能夠在子類的構造函數中調用父類的構造函數了。

這樣就有了組合繼承這種方式:

function Person(name, age){
    this.name = name;
    this.age = age;
}    

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old!");
}

function Teacher(name, age, staffId){
    Person.call(this, name, age);        // 經過call方法來調用父類的構造函數進行初始化
    this.staffId = staffId;
}

Teacher.prototype = new Person();
Object.defineProperty(Teacher.prototype, "constructor", {
    enumerable: false,
    value: Teacher
});

var will = new Teacher("Will", 28, 1000);
will.getInfo();

console.log(will.__proto__);
// Person {name: undefined, age: undefined}
console.log(will.__proto__.constructor);
// function Teacher

在這個例子中,在子類構造函數"Teacher"中,直接經過"Person.call(this, name, age);"的方式調用了父類的構造函數,進而設置了"name"和"age"屬性(但這裏依舊是覆蓋了父類的"name"和"age"屬性)。

組合式繼承是比較經常使用的一種繼承方法,其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又保證每一個實例都有它本身的屬性。

組合式繼承的小問題

雖然組合繼承是 JavaScript 比較經常使用的繼承模式,不過經過前面組合繼承的代碼能夠看到,它也有一些小問題。

首先,子類會調用兩次父類的構造函數:

  • 一次是在建立子類型原型的時候
  • 另外一次是在子類型構造函數內部

子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類型構造函數時重寫這些屬性,從下圖能夠看到"will"對象中有兩份"name"和"age"屬性。

後面,咱們會看到如何經過"寄生組合式繼承"來解決組合繼承的這個問題。

原型式繼承

在前面兩種方式中,都須要用到對象以及建立對象的構造函數(類型)來實現繼承。

可是在JavaScript中,建立對象徹底不須要定義一個構造函數(類型),經過字面量的方式就能夠建立一個自定義的對象。

爲了實現對象之間的直接繼承,就有了原型式繼承

這種繼承方式方法並無使用嚴格意義上的構造函數,而是直接藉助原型基於已有的對象建立新對象,同時還沒必要建立自定義類型(構造函數)。爲了達到這個目的,咱們能夠藉助下面這個函數:

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

在 "object()"函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例。

下面看看使用"object()"函數實現的對象之間的繼承:

var utilsLibA = {
    add: function(){
        console.log("add method from utilsLibA");
    },
    sub: function(){
        console.log("sub method from utilsLibA");
    }
}

var utilsLibB = object(utilsLibA);

utilsLibB.add = function(){
    console.log("add method from utilsLibB");
}
utilsLibB.div = function(){
    console.log("div method from utilsLibB");
}

utilsLibB.add();
// add method from utilsLibB
utilsLibB.sub();
// sub method from utilsLibA
utilsLibB.div();
// div method from utilsLibB

經過原型式繼承,基於"utilsLibA"建立了一個"utilsLibB"對象,而且能夠正常工做,下面看看對象之間的關係:

經過"object()"函數的幫助,將"utilsLibB"的原型賦值爲"utilsLibA",對於這個原型式繼承的例子,對象關係圖以下,"utilsLibB"的"add"方法覆蓋了"utilsLibA"的"add"方法:

Object.create()

ECMAScript 5 經過新增 "Object.create()"方法規範化了原型式繼承。這個方法接收兩個參數:

  • 一個用做新對象原型的對象
  • 一個爲新對象定義額外屬性的對象(可選的)

在傳入一個參數的狀況下,"Object.create()"與 上面的"object"函數行爲相同。關於更多"Object.create()"的內容,請參考MDN

繼續上面的例子,此次使用"Object.create()"來建立對象"utilsLibC":

utilsLibC = Object.create(utilsLibA, {
    sub: {
        value: function(){
            console.log("sub method from utilsLibC");
        }
    },
    mult: {
        value: function(){
            console.log("mult method from utilsLibC");
        }
    },
})

utilsLibC.add();
// add method from utilsLibA
utilsLibC.sub();
// sub method from utilsLibC
utilsLibC.mult();
// mult method from utilsLibC
console.log(utilsLibC.__proto__);
// Object {add: (), sub: (), __proto__: Object}
console.log(utilsLibC.__proto__.constructor);
// function Object() { [native code] }

寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路,寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。

如下代碼示範了寄生式繼承模式,其實就是封裝"object()"函數的調用,以及對新的對象進行自定義的一些操做:

function create(o){
    var f= object(o);         // 經過原型式繼承建立一個新對象
    f.run = function () {     // 以某種方式來加強這個對象
        return this.arr;
    };
    return f;                 // 返回對象
}

寄生組合式繼承

所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。

其背後的基本思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所須要的無非就是父類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承父類型的原型,而後再將結果指定給子類型的原型。

注意在寄生組合式繼承中使用的「inheritPrototype()」函數。

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) { var prototype = object(superType.prototype);    // 建立對象
    prototype.constructor = subType;                // 加強對象,設置constructor屬性
    subType.prototype = prototype;                  // 指定對象
} function Person(name, age){
    this.name = name;
    this.age = age;
}    
Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old!");
}


function Teacher(name, age, staffId){
    Person.call(this, name, age)
    this.staffId = staffId;
}

inheritPrototype(Teacher, Person);

Teacher.prototype.getId = function(){
    console.log(this.name + "'s staff Id is " + this.staffId);
}


var will = new Teacher("Will", 28, 1000);
will.getInfo();
// Will is 28 years old!
will.getId();
// Will's staff Id is 1000

var wilber = new Teacher("Wilber", 29, 1001);
wilber.getInfo();
// Wilber is 29 years old!
wilber.getId();
// Wilber's staff Id is 1001

代碼中有一處地方須要注意,給子類添加"getId"方法的代碼("Teacher.prototype.getId")必定要放在"inheritPrototype()"函數調用以後,由於在「inheritPrototype()」函數中會重寫「Teacher」的原型。

下面繼續查看一下對象"will"的原型和"constructor"屬性。

 

這個示例中的" inheritPrototype()"函數實現了寄生組合式繼承的最簡單形式。這個函數接收兩個參數:子類型構造函數和父類型構造函數。

在函數內部,第一步是建立超類型原型的一個副本。第二步是爲建立的副本添加 "constructor" 屬性,從而彌補因重寫原型而失去的默認的 "constructor" 屬性。最後一步,將新建立的對象(即副本)賦值給子類型的原型。這樣,咱們就能夠用調用 "inheritPrototype()"函數的語句,去替換前面例子中爲子類型原型賦值的語句了("Teacher.prototype = new Person();")。

對於這個寄生組合式繼承的例子,對象關係圖以下:

總結

本文介紹了JavaScirpt中的 幾種經常使用繼承方式,咱們能夠經過構造函數實現繼承,也能夠直接基於現有的對象來實現繼承。

不管哪一種繼承的實現,本質上都是經過JavaScript中的原型特性,結合原型鏈的搜索實現繼承。

與其說"JavaScript是一種面向對象的語言",更恰當的能夠說"JavaScript是一種基於對象的語言"。

經過了這些介紹,相信你必定對JavaScript的繼承有了一個比較清楚的認識了。

相關文章
相關標籤/搜索