JavaScript中繼承的那些事

引言

JS是一門面向對象的語言,可是在JS中沒有引入類的概念,以前特別疑惑在JS中繼承的機制究竟是怎樣的,一直學了JS的繼承這塊後才恍然大悟,遂記之。數組

假如如今有一個「人類」的構造函數:app

function Human() {
    this.type = '人類';
}
還有一個「男人」的構造函數:
function Man(name,age) {
    this.name = name;
    this.age = age;
}
如今咱們想實現的是,讓這個「男人」繼承「人類」。

 

 

借用構造函數

咱們能夠經過在子類型的內部調用超類型的構造函數來達到子類型繼承超類型的效果。函數是在特定環境中執行的代碼對象,所以能夠經過call()或者apply()在新建立的對象上執行構造函數。函數

        function Man(name, age) {
            Human.apply(this,arguments);
            this.name = name;
            this.age = age; 
        }

        var man = new Man('shanlei',20);
        console.log(man.type);
        //輸出:人類
在代碼中的Human.apply(this,arguments)這一段代碼借調用超類型的構造函數,經過apply函數(或call()函數),其實是在(將要)新建立的Man實例的環境下調用超類型構造函數,這樣就會在Man實例對象上執行Human()函數中定義的全部對象初始化代碼,這樣的話Man中每一個實例就會都有本身的type屬性的副本了。

不過借用構造函數進行繼承,不免會有方法都在構造函數中定義,沒法實現函數的複用。而且在超類型的原型中定義的方法對於子類型是不可見的,結果全部類型都要使用構造函數進行繼承,因此單獨使用構造函數的狀況比較少。this

 

 

經過原型鏈

原型鏈的定義機制我在JavaScript中原型鏈的那些事中已經提過了,說到底,經過原型鏈實現繼承根本是經過prototype屬性進行實現的。spa

若是Man的原型指向的是Human的實例,那麼Man原型對象中將會包括一個指向Human的指針,那麼全部Man的實例就均可以繼承Human了。.net

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

        Man.prototype = new Human();
        Man.prototype.constructor = Man;
        var man = new Man('shalei',20);
        console.log(man.type);

咱們知道其實prototype屬性實質上就是一個指向函數對象的指針,咱們經過改變prototype屬性的指向,讓他指向Human的一個實例。prototype

Man.prototype.constructor = Man;
咱們都知道任意一個原型對象都有一個constructor屬性,constructor屬性指向了它的構造函數,也就是說,若是沒有改變Man的prototype的指向,那麼Man.prototype.constructor是指向Man的。
更重要的是,每個實例也有一個constructor屬性,實例的constructor屬性默認調用prototype的constructor,即:
console.log(man1.constructor == Man.prototype.constructor);
//輸出:true
因此想一下當在執行完Man.prototype = new Human()後,全部的Man實例都指向了Human屬性,即:
console.log(man1.constructor == Human);
//輸出:true
這樣的話會致使原型鏈的紊亂,也就是說原型鏈將會中斷,因此咱們必須手動糾正。

咱們改變了Man的prototype的指向,讓他等於一個Human的實例對象。即:指針

Man.prototype.constructor = Man;
這是很是重要的一步,若是咱們在代碼中更換了prototype對象,那麼爲了避免破壞原型鏈,下一步必作的就是糾正prototype的constructor屬性,讓這個屬性指回原來的構造函數。

 

 

組合繼承(僞經典繼承)

組合繼承的總體思想就是將原型鏈和借用構造函數同時使用,取二者的長處的一種繼承模式。思路是使用原型鏈實現原型屬性和方法的繼承,借用構造函數來實現對實例屬性的繼承。這樣作的好處是實現了函數的複用,同時又保證了每一個屬性都有本身的屬性。code

那麼上面讓「男人」繼承「人類」就能夠經過組合繼承實現:對象

        Human.prototype.go = function() {
            console.log('running!');
        }

        function Man(name, age) {

            Human.apply(this,arguments);
            this.name = name;
            this.age = age;
        }

        Man.prototype = new Human();
        Man.prototype.constructor = Man;

        var man1 = new Man('shanlei',20);
        console.log(man1.type);
        man1.type = 'man';
        console.log(man1.type);
        console.log(man1.name);
        console.log(man1.age);


        var man2 = new Man('zhangkai',18);
        console.log(man2.type);
        console.log(man2.name);
        console.log(man2.age);
        man2.go();

輸出以下:

人類
man
shanlei
20
人類
zhangkai
18
running!

 

原型式繼承

若是說繼承的對象並非構造函數呢?咱們沒有辦法使用借用構造函數進行繼承,這個時候咱們就可使用原型式繼承。

這個繼承模式是由道格拉斯·克羅克福德提出的。原型式繼承並無使用嚴格意義上的構造函數。而是藉助原型能夠在已有的對象上建立新的對象,同時還避免了建立自定義類型。因此道格拉斯·克羅克福德給出了一個函數:

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

咱們能夠建立一個新的臨時性的對象來保存超類型上全部屬性和方法,用來給子類型的繼承。而這個就是這個函數要作的事。

在object函數內部先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的新實例。本質上其實就是object對傳入的對象進行了一次淺複製。

如今有一個「男人」對象:

        var man = {
            name: 'shanlei',
            age:20
        }
還有一個「人類」對象:
        var human = {
            type:'人類'
        }
如今我想讓這個man繼承human,也就是說這個「男人」,他是一個「人類」。

這裏須要注意的是,這兩個對象如今時普通的對象,而不是構造函數,因此咱們沒法使用上面的方法。

咱們能夠經過原型式繼承,以下面的例子:

        var human = {
            type:'人類'
        }

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

        var ano_man = object(human);
        ano_man.name = 'shanlei';
        ano_man.age = 20;
        console.log(ano_man.type);
原型式繼承要求必須有一個對象能夠做爲另外一個對象的基礎,若是有的話,那麼只須要將它傳給object函數,而後在根據需求對獲得的對象進行修改就好了。在上面的例子中object函數返回一個新對象,這個新對象以human爲原型,因此它的原型中就包含一個基本類型值屬性。

在ECMAScript中經過函數Object.create()規範了原型式繼承,該方法接收兩個參數,一個用於新對象原型的對象,第二個參數用於爲新對象定義額外屬性的對象,在傳入一個參數的狀況下,Object.create()和上面的object()函數做用相同。

 

 

拷貝繼承

咱們能夠想一下其實繼承的意思就是子類型把超類型的全部屬性和方法拿過來放在本身身上。 那麼咱們能夠將超類型的屬性和方法所有拷貝給子類型,從而實現繼承。

淺拷貝

咱們能夠實現一個方法,將超類型的對象傳入方法,而後將對象的屬性添加到子類型上並返回,具體代碼以下:

        function extendCopy(val) {
            var c = [];
            for(var i in val) {
                c[i] = val[i];
            }
            c.uber = val;
            return c;
        }

具體使用能夠這樣:

        var ano_man = extendCopy(human);
        ano_man.name = 'shanlei';
        console.log(ano_man.type);

使用方法相似於上面介紹的原型式繼承。可是這樣實現繼承有一個很大的問題,那就是當對象的屬性是引用類型值(數組,對象等)時,在拷貝過程當中,子對象得到的只是一個內存地址,而不是真正的屬性拷貝。

 

深拷貝

咱們能夠在淺拷貝的基礎上進行深拷貝。咱們知道,當在拷貝基本類型值時是在內存中新開闢了一塊區域用於拷貝對象屬性的存儲,因此咱們只須要遞歸下去調用淺拷貝就好了。

    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;
  }

使用方法和淺拷貝相似,這裏就不舉例了。

 

 

寄生式繼承

寄生式繼承的思路就是建立一個用於封裝繼承過程的函數,在該函數內部以某種方式來加強對象,最後再向真的它作了全部工做同樣返回對象。仍是上面man和human兩個對象間實現繼承的例子:

function createAnother(original){
    var clone = object(original);   //經過調用函數建立一個新對象
    clone.sayHi = function(){       //以某種方式來加強這個對象
        alert("hi");
    };
    return clone;                  //返回這個對象
}
使用時:
var clone = createAnother(human);
clone.sayHi();

在主要實現對象是自定義類型而不是構造函數的狀況下,寄生式繼承是一種有用的繼承模式,其中使用的object函數不是必須的,任何能實現該功能的函數均可以。

 

 

寄生組合式繼承

組合繼承是JS中一種很是經常使用的繼承模式,但是這個方式實現繼承有一個問題,就是不管在任何狀況下,都會調用兩次超類型構造函數。一次是在建立子類型原型的時候,第二次是在子類型構造函數內部。子類型最終會包含超類型對象的所有實例屬性,可是咱們不得不在調用子類型構造函數時重寫這些屬性。如此在繼承很是頻繁的狀況下就會形成內存過分損耗的狀況了。這個時候,咱們可使用寄生組合式繼承!

寄生組合式繼承,就是借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。具體思路是沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所須要的無非就是超類型原型的一個副本而已,本質上,就是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型。

基本模式以下:

        function inheritPrototype(subType, superType){
            var prototype = object(superType.prototype);   //建立對象
            prototype.constructor = subType;               //加強對象
            subType.prototype = prototype;                 //指定對象
        }
該函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步時建立一個超類型原型對象的副本 。第二步爲建立的副本添加constructor屬性,從而彌補因重寫原型而失去默認的constructor屬性。最後一步將新建立的副本對象複製給子類型的原型。

讓咱們回到第一個問題:有一個「男人」的構造函數和「人類」的構造函數,我如今想讓男人繼承人類!

        function Human() {
            this.type = '人類';
        }

        Human.prototype.sayHi = function() {
            console.log('hi');
        }

        function Man(name,age) {
            Human.apply(this,arguments);
            this.name = name;
            this.age = age;
        }

        inheritPrototype(Man, Human);

        var man = new Man('shalei',20);

        console.log(man.type)
        man.sayHi();

 

寄生組合式繼承是引用類型最理想的的繼承範式!

 

以上~

相關文章
相關標籤/搜索