1. 程式人生 > >JavaScript中的類繼承

JavaScript中的類繼承


// 1 原型繼承
// ----------------------------------------------------------------------------------------
// 建構函式、原型和例項的關係:
// 每個建構函式都有一個【原型物件】,原型物件都包含一個指向【建構函式的指標】,
// 而“例項”都包含一個指向【原型物件的內部指標】。

// 原型鏈:
// 假如我們讓原型物件等於另一個型別的例項,結果會怎麼樣呢?顯然,此時的原型物件將包含一個指向另一個原型的指標,
// 相應地,另一個原型中也包含著一個指向另一個建構函式的指標。
// 假如另一個原型又是另一個型別的例項,那麼上述關係依然成立,如此層層遞進,就構成了例項與原型的鏈條。
// 這就是所謂原型鏈的基本概念。
// 原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中,最主要的問題來自包含引用型別值的原型。
// 想必大家還記得,我們前面介紹過包含引用型別值的原型屬性會被所有例項共享;而這也正是為什麼要在建構函式中,而不是在原型物件中定義屬性的原因。
// 在通過原型來實現繼承時,原型實際上會變成另一個型別的例項。於是,原先的例項屬性也就順理成章地變成了現在的原型屬性了。
// 原型鏈的第二個問題:在建立子型別的例項時,不能向超型別的建構函式中傳遞引數。
// 實際上,應該說是沒有辦法在不影響所有物件例項的情況下,給超型別的建構函式傳遞引數。
// 有鑑於此,再加上前面剛剛討論過的由於原型中包含引用型別值所帶來的問題,實踐中很少會單獨使用原型鏈。


// 優點:實現簡單
// 缺點:
// 1.無法向父類建構函式傳參,父類只能例項傳參,不符合常規語言的寫法
// 2.同時new兩個物件時改變一個物件的原型中的引用型別的屬性時,
// 另一個例項的該屬性也會修改。因為來自原型物件的引用屬性是所有例項共享的。
var

obj = { // 父類的原型物件屬性
    0:'a',
    1:'b',
    arr:[1]
};
obj.prototype.id = 10; // 父類的原型物件屬性

function Foo(){
    this.arr2 = [1];
}
Foo.prototype = obj; // 原型繼承關鍵程式碼




// 使用說明
var foo1 = new Foo();
var foo2 = new Foo();
foo1.arr.push(2);
foo1.arr2
.push(2);

console.log(foo2.arr);  //[1,2],原型中的引用型別的屬性【類例項的共享屬性】被修改時,所有例項的該原型引用屬性也將同步修改。
console.log(foo2.arr2); //[1]

// 1+ 原型式繼承
// 在沒有必要興師動眾地建立建構函式,而只想讓一個物件與另一個物件保持類似的情況下,原型式繼承是完全可以勝任的。
// 不過別忘了,包含引用型別值的屬性始終都會共享相應的值,就像使用原型模式一樣。
function object(o){
    function F(){}
    F.prototype
= o;
    return new F();
}




// 2 構造繼承
// ----------------------------------------------------------------------------------------
// 有時候也叫做【偽造物件或經典繼承】,即在子型別建構函式的內部呼叫超型別建構函式。
// 別忘了,【函式只不過是在特定環境中執行程式碼的物件】,因此通過使用apply()和call()方法也可以在(將來)新建立的物件上執行建構函式。
// 如果僅僅是借用【建構函式】,那麼也將無法避免建構函式模式存在的問題—---方法都在建構函式中定義,因此函式複用就無從談起了。
// 而且,【在超型別的原型中定義的方法,對子型別而言也是不可見的】,結果所有型別都只能使用建構函式模式。考慮到這些問題,借用建構函式的
// 技術也是很少單獨使用的。

// 優點:可以向父類傳參,子類不會共享父類的引用屬性。
// 缺點:無法實現函式複用,每個子類都有新的fun,太多了就會影響效能。不繼承父類的原型物件。
function Super1(b){
    this.b = b;
    this.fun = function(){};
}
function Foo1(a,b){
    // 建構函式中
    this.a = a;

    // 構造繼承向父類傳參
    Super1.call(this,b); // 但是通過這種方式例項化出來的例項會將父類方法多次儲存,影響效能
}

// usage
var foo1_obj = new Foo1(1,2);
console.log(foo1_obj.b);
console.log(foo1_obj.a);


// 3 組合繼承
// ----------------------------------------------------------------------------------------
// combination inheritance, 有時候也叫做偽經典繼承,指的是將原型鏈和借用建構函式的技術組合到一塊,從而發揮二者之長的一種繼承模式。
// 其背後的思路是【使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對例項屬性的繼承】。
// 這樣,既通過在原型上定義方法實現了函式複用,又能夠保證每個例項都有它自己的屬性。
// 組合繼承避免了原型鏈和借用建構函式的缺陷,融合了它們的優點,<<<<成為JavaScript 中最常用的繼承模式。>>>>>
// 而且,instanceof 和isPrototypeOf()也能夠用於識別基於組合繼承建立的物件。

// 優點:不存在引用屬性共享問題,可傳參,函式可複用
// 缺點:父類的屬性會被例項化兩次
function Super2(){
    // 只在此處宣告基本屬性和引用屬性
    this.val = 1;
    this.arr = [1];
}

//  在此處宣告函式
Super2.prototype.fun1 = function(){};
Super2.prototype.fun2 = function(){};

//Super.prototype.fun3...
function Sub2(){
    Super2.call(this);   // 組合繼承核心----構造繼承,建構函式內繼承屬性
    // ...
}
Sub2.prototype = new Super2(); // 組合繼承核心----原型繼承,繼承方法【函式又例項一次,函式內部變數又重複例項一次】


// 說明
// 但是,通過這種方式例項化出來的物件會儲存兩份父類建構函式中的屬性
var sub2_obj = new Sub2();


//4 寄生組合繼承 【最佳】
// ----------------------------------------------------------------------------------------
// 【通過借用(建構函式)來繼承屬性,通過(原型鏈的混成形式)來繼承方法。】
// 其背後的基本思路是:不必為了指定子型別的原型而呼叫超型別的建構函式,我們所需要的無非就是超型別原型的一個副本而已。
// 本質上,就是使用(寄生式繼承)來繼承超型別的原型,然後再將結果指定給子型別的原型。
// 基本模式如下所示:
function inheritPrototype(subType, superType){
    var obj = object(superType.prototype); //建立物件
    obj.constructor = subType; //增強物件
    subType.prototype = obj; //指定物件
}
// 引數:子型別建構函式和超型別建構函式。
// 第一步,在函式內部,建立超型別原型的一個副本。
// 第二步,為建立的副本新增constructor 屬性,從而彌補因重寫原型而失去的預設的constructor屬性。
// 最後一步,將新建立的物件(即副本)賦值給子型別的原型。這樣,我們就可以用呼叫inheritPrototype()函式的語句,
// 去替換前面例子中為子型別原型賦值的語句了。 集寄生式繼承和組合繼承的優點與一身,是實現基於型別繼承的最有效方式。


// 使用示例:
function Super4(b){
    this.b = b;
}
Super4.prototype.c = function(){console.log(1111);};

//對父類的prototype進行一次寄生,即包裝成一個空物件的prototype,
var f = new Function();
f.prototype = Super4.prototype; // 把這個物件例項化出來

// 繼承的程式碼段
function Sub4(a,b){
    this.a = a;
    Super4.call(this,b);  // 子類構造繼承父類
}
Sub4.prototype = new f();// 作為子類的prototype

// 建立完子類Sub4後的使用
var sub4_obj = new Sub4(1,2);


// 說明:
// 用父類的原型構造一個新物件作為子類的原型,就解決了多次儲存的問題。
// 最終的寄生組合繼承就是最佳繼承方式,它的缺點就是書寫起來比較麻煩。
// 對父類的prototype進行一次寄生,即包裝成一個空物件的prototype,
// 再把這個物件例項化出來作為子類的prototype。






// 總結
// ----------------------------------------------------------------------------------------
/*
繼承主要是實現子類對父類方法、屬性的複用。
來自原型物件的引用屬性是所有例項共享的,所以我們要避免從原型中繼承屬性。
在建構函式中通過call函式可以繼承父類建構函式的屬性和方法,但是通過這種方式例項化出來的例項會將父類方法多次儲存,影響效能。
通過組合繼承我們使用call繼承屬性,使用原型繼承方法,可以解決以上兩個問題,但是通過這種方式例項化出來的物件會儲存兩份父類建構函式中的屬性。
用父類的原型構造一個新物件作為子類的原型,就解決了多次儲存的問題,所以最終的寄生組合繼承就是最佳繼承方式,它的缺點就是書寫起來比較麻煩。
*/



// DEMO1:
var Zepto = (function(){

    var $,zepto = {};

    // ...省略N行程式碼...

    $ = function(selector, context){
        return zepto.init(selector, context);
    };

    zepto.init = function(selector, context) {
        var dom;
        // ...
        return zepto.Z(dom, selector);
    };

    function Z(dom, selector){
        //...
    }

    zepto.Z = function(dom, selector) {
        return new Z(dom, selector);
    };

    $.fn = {
        // 裡面有若干個工具函式
    };

    zepto.Z.prototype = Z.prototype = $.fn;

    // ...省略N行程式碼...

    return $;
})();

// 我們呼叫$(‘..’)時其實是呼叫 zepto.z 函式例項化了一個物件出來(具體不做分析),
// 物件的建構函式是Z函式,通過 Z.prototype = $.fn 來實現物件對工具方法的繼承。
// 因為zepto物件主要是使用它的一些工具方法,不需要對屬性繼承,
// 也不需要對父類構造進行傳參,所以原型繼承完全滿足需要,而且寫法簡單。



// node.js中的繼承
// node.js中的各個物件屬性和方法都非常多,因此繼承時就使用我們的最優繼承方式:寄生組合繼承。
function Stream(){
    //...
}

function OutgoingMessage() {
    Stream.call(this); //通過call繼承Stream構造中的屬性。
    //...
}
OutgoingMessage.prototype.setTimeout = function () {
    // 擴充套件OutgoingMessage自身原型的函式。
    // ...
};

//呼叫inherits方法繼承Stream原型中的屬性
function inherits(ctor, superCtor) {
    ctor.super_ = superCtor;
    // 使用了Object.create方法,該方法的作用是通過指定的原型物件和屬性建立一個新的物件。
    // 該方法實際上就做了我們上面寄生組合繼承中的工作
    // var f = new Function();
    // f.prototype =superCtor.prototype;
    // return new f();
    // 後面的引數是給原型物件新增屬性,可選屬性(非必填),即把自身作為新建立物件的建構函式。
   /*value: 表示constructor 的屬性值;
    writable: 表示constructor 的屬性值是否可寫;[預設為: false]
    enumerable: 表示屬性constructor 是否可以被列舉;[預設為: false]
    configurable: 表示屬性constructor 是否可以被配置,例如 對obj.a做 delete操作是否允許;[預設為: false]*/
    ctor.prototype = Object.create(superCtor.prototype, {
        constructor: {
            value: ctor,
            enumerable: false,
            writable: true,
            configurable: true
       
}
    });
}

inherits(OutgoingMessage, Stream);


// 以上是寄生組合繼承的一個例項。
// 1.在OutgoingMessage建構函式中通過call繼承Stream構造中的屬性。
// 2.呼叫inherits方法繼承Stream原型中的屬性。
// 3.擴充套件OutgoingMessage自身原型的函式。
// inherits方法中使用了Object.create方法,該方法的作用是通過指定的原型物件和屬性建立一個新的物件。


// 參考文章
// https://blog.csdn.net/qq_34149805/article/details/79221431
// JavaScript面向物件程式設計(繼承實現方式)  https://blog.csdn.net/hsd2012/article/details/50980270

// 備註,花費了大量時間整理,如有幫助,請給作者點贊或關注支援!