關於JavaScript繼承的那些事


在JavaScript中,對象的創建可以脫離類型(class free),通過字面量的方式可以很方便的創建出自定義對象。

另外,JavaScript中擁有原型這個強大的概念,當對象進行屬性查找的時候,如果對象本身內找不到對應的屬性,就會去搜索原型鏈。所以,結合原型和原型鏈的這個特性,JavaScript就可以用來實現對象之間的繼承了。

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

原型鏈繼承

由於原型鏈搜索的這個特性,在JavaScript中可以很方便的通過原型鏈來實現對象之間的繼承。

下面看一個例子:

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"的方法。

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

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

 

constructor屬性

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

對於"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的繼承有了一個比較清楚的認識了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM