JavaScript繼承

概況

《Object Oriented JavaScript》提及了12種javascript的繼承方式的變化(12種,感覺有點多吧).

JavaScript中並沒有類,function在JavaScript中的作用只是作爲一個構造函數,不過我們後面都暫且把構造函數叫做類。我們認爲一個實例的屬性依賴於其構造函數提供的屬性配置,以及構造函數的原型(prototype)的屬性。

要做到繼承就要先利用好這兩個因素。

從簡單的例子開始

先聲明一個Animal構造函數,用於創建一個動物的實例。

複製代碼
function Animal() {
    this.name = "Animal";
    this.color = "";
    this.legsNumber = 4;
}

// 在原型鏈上聲明一個shout方法
Animal.prototype.shout = function() {
    this.name && alert("I am a " + this.name);
    this.color && alert("My Color is " + this.color);
    this.legsNumber && alert("I Have " + this.legsNumber + " legs");
};
複製代碼

 


然後我們從Animal類衍生出一類Cat的貓科動物類通常我們會怎麼寫呢?由於JavaScript是原型繼承的,我們會把新的構造函數的prototype指向由 父類 的創建的一個實例。

複製代碼
function WhiteCat() {
    this.name = "Cat";
    this.color = "white";
}

WhiteCat.prototype = new Animal();
複製代碼

 

 

我們生成一個WhiteCat實例看看,可以發現這時候他有了一個shout方法。

看了這一段代碼可能會讓人覺得很弱…不過至少這裏WhiteCat繼承了Animal類的shout方法。總之,這就是最基礎的繼承。

注意點


WhiteCat.prototype =newAnimal();

 

 

注意在這句代碼中,我們已經將WhiteCat的原型完全重寫了,原本WhiteCat.prototype.constructor是指向構造器本身的,經過重寫這個鏈就斷掉了,我們可以通過手寫的方式補回這個鏈


WhiteCat.prototype.constructor =WhiteCat;
 

再抽象一些

上一個例子比較具體地展示了一個類繼承於另一個類的過程。

我們把它抽象一下,編寫一個extend函數專門處理這個繼承過程,這個函數接受兩個參數,子類和父類。

var extend = function(Child, Parent) {
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;
};

 

使用這個函數就可以實現類之間的繼承。

複製代碼
function Animal() {
    this.name = "Animal";
    this.color = "";
    this.legsNumber = 4;
}

Animal.prototype.shout = function() {…}

function WhiteCat() {
    this.name = 'whitecat';
}

extend(WhiteCat,Animal);
複製代碼

 

只繼承prototype部分

前面的繼承我們把父類的實例屬性和原型屬性都繼承了過來。 

如何只繼承原型的部分,很簡單。

var extendPrototype = fucntion(Child, Parent) {
    Child.prototype = Parent.prototype;
    Child.prototype.constructor = Child;
} 

再試試Cat繼承Animal的過程,可以發現Animal的legsNumber屬性是沒有繼承過來的。

使用new F()

不知道有沒有看出來上面的繼承過程有一個問題,我們把子類的原型指向父類的原型,他倆公用同一個原型對象,一旦我們更改了子類原型上面的某一方法,父類也會受到影響。

因此我們要做一些調整,使用一個空的構造器來隔離開他們兩個。

複製代碼
var extendPrototype2 = function(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
} 
複製代碼

如何從子類訪問父類

爲了在子類中讀取父類的方法,我們要手動在子類上設定一個屬性指向其父類。


複製代碼
var extendPrototype2 = function(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.super = Parent.prototype;
}
複製代碼


至此我們基本完成了一個最基本的繼承。

換一種方式繼承

採用原型鏈的方式可以實現繼承

除了子類繼承父類的原型屬性,我們還可以把父類原型的屬性複製到子類的原型上面。


複製代碼
var extend2 = function(Child, Parent) {
    var p = Parent.prototype;
    var c = Child.prototype;
    for (var i in p) {
        c[i] = p[i]; 
    }
    c.uber = p;
}
複製代碼

這種方式不同於之前的繼承在於,之前如果子類自身如果沒有定義一些屬性,對應的屬性查找就會延伸到父類和父類的原型。 

而這種繼承直接複製了父類原型的屬性(不過如果複製的屬性是對象話還是會使用指針的方式,我們會在後面提到深度繼承來解決這個問題)。

繼承自對象的對象

從上面的這種繼承方式,我們可以衍生出一個簡單的繼承方式,其實Parent.prototypeChild.prototype本質都是對象。上面的繼承方式可以直接改造爲對象之間的繼承。


複製代碼
var extendCopy = function(o) {
    var c = {};
    for (var i in p) {
        c[i] = p[i]; 
    }
    c.uber = p;
    return c;
}
複製代碼

深度屬性拷貝

前面的繼承存在一種問題,如果在父類的原型上面存在一個對象或者數組型的屬性。那麼在被子類的原型複製後,修改子類原型的同名屬性,父類的原型可能會被修改。
所以我們要做一種原型的深度拷貝,直到拷貝的屬性值是基本類型。


複製代碼
function deepCopy(p, c) {
    var c = c || {};
    for (var i in p) {
        if (typeof p[i] === 'object') {
            c[i] = (p[i].constructor === Array) ? [] : {};
            deepCopy(p[i], c[i]);
        } else {
            c[i] = p[i];
        }
    }
    return c;
}
複製代碼

DC對屬性拷貝的建議

我們之前通過拷貝對象屬性來達到繼承,著名的老道對於裏面的子類原型部分的代碼,對於對象的創建,他建議使用一個F函數的構造器來代替對象字面量(可見javascript語言精粹第三章),並把其實例作爲結果返回。於是他寫了這樣的一個object函數,其參數作爲新構造器的原型。


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

使用原型繼承和屬性拷貝相結合

我們使用繼承往往是先繼承一個已有的對象,然後會在其基礎上面再做一些修改。到代碼的層面差不多是先做一次繼承,然後再對實例添加一些額外的方法。
這時候把原型繼承和屬性拷貝結合起來就很有意義。


複製代碼
function objectPlus(o, stuff) {
    var F = function(){};
    F.prototype = o;
    var c = new F();
    c.uber = o;

    for (var i in stuff) {
        c[i] = stuff[i];
    }
    return c;
}
複製代碼

多重的繼承

我們還可以從多個父對象來繼承我們的子對象,multi接受的參數是多個對象。


複製代碼
function multi() {
    var c = {},// 或者使用new F()的方式
        len = arguments.length,
        stuff;
        for (var i = 0; i < len; i++) {
            stuff = arguments[i];
            for (var k in sutff) {
                c[k] = stuff[k];
            }
        }
    return c;
}
複製代碼

這種多重的繼承還可以用於對象的Mixin。

借用父類的構造器來進行繼承

除了繼承父類的方法和屬性我們還可以使用父類的構造器來完成子類的實例的構造。
在js中存在callapply兩種用於靈活調用函數的方法,藉助他們我們就可以直接在子類的實例化過程中去調用父類的構造器,從而完成繼承的過程。

我們先用具體的代碼來實現這個過程,然後再進行抽象。

還是先聲明一個Animal的類。


複製代碼
function Animal(config){
    this.name = "Animal";
    this.color = "";
    this.legsNumber = 4;
}

// 在原型鏈上聲明一個shout方法
Animal.prototype.shout = function() {
    this.name && alert("I am a " + this.name);
    this.color && alert("My Color is " + this.color);
    this.legsNumber && alert("I Have " + this.legsNumber + " legs");
};
複製代碼

再聲明我們的子類


複製代碼
function WhiteCat() {
    Animal.apply(this);// 這裏我們在子類直接調用Animal的構造器
    this.name = "Cat";
    this.color = "white";
}

WhiteCat.prototype = new Animal();
複製代碼
 

這樣就簡單的借用了父構造器來繼承。

我們再把這個過程抽象一下


複製代碼
function extendCallParent(Child, Parent) {
    Child.prototype = new Parent();
    Child.prototype.contructor = Child;
    Child.super = Parent;
}

function Animal(config){
    this.name = "Animal";
    this.color = "";
    this.legsNumber = 4;
}

// 在原型鏈上聲明一個shout方法
Animal.prototype.shout = function() {
    // ...
};

function WhiteCat() {
    WhiteCat.super.constructor.apply(this, arguments);
    this.name = "Cat";
    this.color = "white";
}

extendCallParent(WhiteCat, Animal);
複製代碼

借用父構造器的一種改造

上面的繼承過程中我們兩次調用了父類的構造器。

如果我們只繼承父類原型上面的屬性的話,可以不做Child.prototype = new Parent()這一步。 

取而代之的是使用原型屬性複製的方式。


function extendCallParent(Child, Parent) {
    Child.prototype = Parent.prototype;
    Child.prototype.contructor = Child;
    Child.super = Parent.prototype;
}

CoffeeScript中的繼承 - JavaScript繼承的實際應用

CoffeeScript是對JavaScript的語法的一個很好的約束的工具和語言,它定義了一套獨立於JavaScript的語法,確保你能安全高效的編寫js程序,不至於輕易的犯錯。
你可以從CoffeeScript編譯出對應的JavaScript語法。CoffeeScript關注的是你寫代碼的過程,讓你編寫更簡潔明晰的代碼。而讓解析引擎編譯出對應的js腳本。想詳細的瞭解CoffeeScript你可以參考其官網和一些資料

CoffeeScript中的繼承語法很簡單,不過有一點要注意的是在CoffeeScript中縮進是有含義的,例如下面的這個例子


複製代碼
class Animal
    constructor: (@name) ->

    alive: ->
        false

class Parrot extends Animal
    constructor: ->
        super("Parrot")

    dead: ->
        not @alive()
複製代碼

這是一個從Animal擴展出Parrot類的例子。Animal類具有實例的構造函數和alive方法(返回爲false)。然後我們定義了Parrot類並制定它繼承自Animal類,我們使得Parrot類的構造函數直接調用父類的構造函數。Parrot實例的dead方法則直接調用的繼承來的實例alive方法。

這是一個基本的例子。這一段CoffeeScript會被編譯成什麼樣的JavaScript代碼呢?


複製代碼
var Animal, Parrot,
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) {
        for (var key in parent) {
            if (__hasProp.call(parent, key))
                child[key] = parent[key];
        }
        function ctor() {
            this.constructor = child;
        }
        ctor.prototype = parent.prototype;          child.prototype = new ctor();
        child.__super__ = parent.prototype;

        return child;
    };

Animal = (function() {

    function Animal(name) {
        this.name = name;
    }

    Animal.prototype.alive = function() {
        return false;
    };

    return Animal;

})();

Parrot = (function(_super) {

    __extends(Parrot, _super);

    function Parrot() {
        Parrot.__super__.constructor.call(this, "Parrot");
    }

    Parrot.prototype.dead = function() {
        return !this.alive();
    };

    return Parrot;

})(Animal);
複製代碼
 

我們可以看到裏面編譯出來的__extend函數。它做的事情就與我們之前說的屬性複製加上new F()的方式類似,不過可以看到子類所複製的屬性都是來自於父類本身靜態方法。

通常在CoffeeScript中,子類一旦繼承於父類,它的實例初始化過程就會調用父類的構造器,當然你也可以重寫其構造函數的過程。

上面的例子中我們就用CoffeeScript中的super方法重寫了對父類構造器的調用的過程。

KISSY的繼承方式

在KISSY庫也有一個extend繼承方法。

看官網的一個例子


複製代碼
var S = KISSY;

function Bird(name) {
    this.name = name;
}
Bird.prototype.fly = function() {
    alert(this.name + ' is flying now!'); 
};

function Chicken(name) {
    Chicken.superclass.constructor.call(this, name);
}
S.extend(Chicken, Bird,{
    fly:function(){
      Chicken.superclass.fly.call(this)
      alert("it's my turn");
    }
});

new Chicken('kissy').fly();
複製代碼

 


可以直接看到KISSY使用的是調用父類構造器的繼承方式。

再看其源碼,extend的實現。

其接受4個參數,子類,父類,要覆蓋的原型方法對象,要覆蓋的靜態方法對象

其extend方法中含有這個的一個create方法,它是用來從已有的父類創建一個新的對象,作爲子類的原型對象。


複製代碼
var create = Object.create ? function (proto, c) {
        return Object.create(proto, {
            constructor:{
                value:c
            }
        });
    } : function (proto, c) {
        function F() {}
        F.prototype = proto;
        var o = new F();
        o.constructor = c;
        return o;
    }
複製代碼

可以看到,這裏使用的方式和我們上面介紹的使用new F()來繼承對象的方式基本一樣,並且還使用了Object.create方法做了對新版JavaScript規範的支持。

KISSY的extend方法中還調用了KISSY的mix方法,大家也可以閱讀一下其實現

總的來說,可以發現,KISSY使用的方式和CoffeeScript使用的繼承方式還是基本一樣的。

總結

JavaScript的繼承主要是源於對原型鏈的利用,我們可以看到最基本的繼承的實現,子類的原型繼承於父類的實例。
我們還可以在此基礎上面做進一步的擴展,我們可以通過複製屬性直接繼承父類的原型,當然考慮到安全性,我們會使用深度的複製和使用一個對象來分隔子類和父類之間的聯繫。另外對於子類的構造器,我們也可以藉助父類的構造器來完成其功能。最後,可以看到其實很多的現有庫的方案都是對我們最基本的繼承方式的一些包裝。

http://cnodejs.org/topic/4fff90fa4764b72902706ad2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章