JavaScript的繼承-轉載

awen1983發表於2019-05-13

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


相關文章