原生JS - ES5繼承

繼承目的:不重復寫類的相同屬性和方法

摘自JavaScript高級程序設計:

繼承是OO語言中的一個最為人津津樂道的概念。許多OO語言都支持兩種繼承方式: 接口繼承實現(xiàn)繼承 。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。由于 js 中方法沒有簽名,在ECMAScript中無法實現(xiàn)接口繼承.ECMAScript只支持實現(xiàn)繼承,而且其 實現(xiàn)繼承 主要是依靠原型鏈來實現(xiàn)的。

原型鏈的問題

原型鏈并非十分完美,它包含如下兩個問題。

問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被所有實例共享;

問題二: 在創(chuàng)建子類型(例如創(chuàng)建Son的實例)時,不能向超類型(例如Father)的構(gòu)造函數(shù)中傳遞參數(shù)。

有鑒于此, 實踐中很少會單獨使用原型鏈。

為此,下面將有一些嘗試以彌補原型鏈的不足。

常見的幾種繼承方式

在開始之前,為了方面后面使用,我們創(chuàng)建一個 Box 類,作為后面被繼承的父類。

function Box(_a) {
    this.a = _a;
    this.play();
    // 靜態(tài)屬性
    Box.static = "static";
}
Box.prototype.aa = 10;
Box.prototype.play = function () {
    // 這里的this指向?qū)嵗瘜ο?    console.log("play")
}

let b = new Box(1);
console.log(b)
/* 打印結(jié)果
{
    a: 1,
    __proto__: {
        aa: 10,
        play: ? (),
        constructor: ? Ball(_a),
        __proto__: Object
    }
}
*/

一、冒充式繼承(借用構(gòu)造函數(shù)繼承)

  • 做法:在子類構(gòu)造函數(shù)的內(nèi)部調(diào)用父類構(gòu)造函數(shù)

  • 優(yōu)點:一舉解決了原型鏈的兩大問題:

    • 其一, 保證了原型鏈中引用類型值的獨立,不再被所有實例共享;
    • 其二, 子類型創(chuàng)建時也能夠向父類型傳遞參數(shù).
  • 缺點:這樣父類會丟失傳入的參數(shù); 還會讓父類構(gòu)造函數(shù)重復執(zhí)行,其中的方法也會重復執(zhí)行(如果不new Ball,也會執(zhí)行一次play())

function Ball(_a) {
    Box.call(this, _a);
}

let b1 = new Ball(20); // TypeError: this.play is not a function
console.log(b1);
console.log(b1.aa); // ===> undefined

/* 注釋Box中的this.play()后打印
{ 
    a: 20,
    __proto__: {
        constructor: ? Ball(_a)
        __proto__: Object
    }
}
*/

可見,b1的原型鏈上沒有找到 play() 這個方法,因此報錯。

二、組合式繼承

將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮兩者之長的一種繼承模式。把子類的 prototype 屬性,指向?qū)嵗母割悺?/p>

  • 做法:使用原型鏈實現(xiàn)對原型屬性和方法的繼承,通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承。
  • 優(yōu)點:組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷
  • 缺點:這樣父類會丟失傳入的參數(shù); 還會讓父類構(gòu)造函數(shù)重復執(zhí)行,其中的方法也會重復執(zhí)行(如果不new Ball,也會執(zhí)行一次play())
function Ball(_a) {
    Box.call(this, _a);
}

Ball.prototype = new Box();
// 原型鏈上沒有constructor,需要加上
Ball.prototype.constructor = Ball;

var b2 = new Ball(10);
console.log(b2);

/*
{
    a: 10,
    __proto__: Box{
        a: undefined, // 不是我們所需要的
        constructor: ? Ball(_a),
        __proto__: {
            aa: 10,
            play: ?(),
            constructor: ? Box(_a),
            __proto__: Object
        }
    }
}
*/

這里有一個需要明確的點:為什么要 new Box() ,而不是Box() 或者 Box ?

  • Box() :這樣會直接執(zhí)行 Box() , this 指向window,而 window 下沒有play()這個方法,并不能執(zhí)行。

    Ball.prototype = Box(); // TypeError: this.play is not a function
    
  • Box: 在Box中剛開始打印this可以發(fā)現(xiàn),實例化的Ball的原型是Box(這似乎滿足我們的需求)。但是 aaplay() 并不在原型鏈上,而在Box的 prototyoe 上,并不能直接訪問到。

    Ball.prototype = Box; // TypeError: this.play is not a function
    

三、原型式繼承

  • 做法:創(chuàng)建一個臨時性的構(gòu)造函數(shù),然后將父類的原型對象作為這個構(gòu)造函數(shù)的原型,最后將臨時類的一個新實例賦值給子類的原型。
  • 優(yōu)點:解決了組合式繼承執(zhí)行兩次 constructor 的問題。
  • 缺點中間類.prototype = 父類.prototype 這個過程相當于做了一次淺復制,父類上的引用類型的屬性值,會被實例化的子類修改。
function Ball(_a) {
    Box.call(this, _a);
}

function F() { }
F.prototype = Box.prototype;

Ball.prototype = new F();
Ball.prototype.constructor = Ball;

let b3 = new Ball(10);
console.log(b3);
/*
{
    a: 10,
    __proto__: Box {
        constructor: ? Ball(_a)
        __proto__:{
            aa: 10,
            play: ? (),
            constructor: ? Box(_a),
            __proto__: Object
        }
    }
}
*/

缺點 - 引用類型被修改的例子:

Box.prototype.arr = [1, 2, 3];
b3.arr.push(4);

let b4 = new Ball(30);
b4.arr.push(5);
console.log(Box.prototype.arr); // [1, 2, 3, 4, 5]

這里有一個需要明確的點:為什么不直接 Ball.prototype = Box.prototype; ?

因為這樣不是真正意義上的繼承, BallBox 二者在原型鏈上沒有了任何聯(lián)系。

驗證:

Ball.prototype = Box.prototype;
Ball.prototype.constructor = Ball;
let b5 = new Ball(10);
console.log(b5);

/*
{
    a: 10,
    __proto__: {
        aa: 10,
        play: ? (),
        constructor: ? Ball(_a),
        __proto__: Object
    }
}
*/

四、寄生式繼承(常用!推薦!)

  • 做法:創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部來實現(xiàn)類的繼承。

  • 優(yōu)點:寄生組合式繼承、集寄生式繼承和組合繼承的優(yōu)點于一身,是ES5實現(xiàn)基于類型繼承的最有效方法

 // 參數(shù):  sub: 子類   sup: 父類
function extend(sub, sup) {    
    // 創(chuàng)建一個中間類
    function F() { }

    // 將父類的原型賦值給這個中間替代類
    F.prototype = sup.prototype;

    // 將原子類的原型保存
    let subProto = sub.prototype;

    // 將子類的原型設置為中間替代類的實例對象
    sub.prototype = new F();

    // 將原子類的原型復制到子類原型上,合并超類原型和子類原型的屬性方法
    Object.assign(sub.prototype, subProto);

    // 設置子類的構(gòu)造函數(shù)時自身的構(gòu)造函數(shù),以防止因為設置原型而覆蓋構(gòu)造函數(shù)
    sub.prototype.constructor = sub;

    // 給子類的原型中添加一個屬性,可以快捷的調(diào)用到父類的原型方法(目的只是為了讓子類剛方便訪問父類的屬性和方法,類似于ES6的super())
    sub.prototype.sup = sup.prototype;

    // 如果父類的原型構(gòu)造函數(shù)指向的不是父類構(gòu)造函數(shù),重新指向
    if (sup.prototype.constructor !== sup) {
        sup.prototype.constructor = sup;
    }
}

/* ===== 使用 extend() ===== */
function Ball(_a) {
    this.sup.constructor.call(this, _a);
}

extend(Ball, Box);

let b6 = new Ball(10);
console.log(b6);

/*
{
    a: 10,
    __proto__: Box {
        constructor: ? Ball(_a),
        sup: {aa: 10, play: ?, constructor: ?}
        __proto__: {
            aa: 10,
            play: ? (),
            constructor: ? Box(_a),
            __proto__: Object
        }
    }
}
*/
4.1 如果需要改寫父類的方法,可以對同名方法進行覆蓋
Ball.prototype.play = function () {
    this.sup.play.call(this);// 如果需要給父類的方法增加內(nèi)容,則先執(zhí)行父類的同名方法
    console.log("end");
}
4.2 Object.assign(sub.prototype, subProto) 的進階寫法

我們知道,constructor應該是不可枚舉的,而使用上面的constructor是可枚舉的,所以這一行代碼,我們可以通過下面的方式進行改寫。

var names = Object.getOwnPropertyNames(subProto);
for (var i = 0; i < names.length; i++) {
    var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
    Object.defineProperty(sub.prototype, names[i], desc);
}
4.3 也可以封裝到Function上,只需要把sub換成this就可以了
Function.prototype.extend = function (sup) {
    function F() { }
    F.prototype = sup.prototype;
    let subProto = this.prototype;
    this.prototype = new F();
    var names = Object.getOwnPropertyNames(subProto);
    for (var i = 0; i < names.length; i++) {
        var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
        Object.defineProperty(this.prototype, names[i], desc);
    }
    this.prototype.constructor = this;
    this.prototype.sup = sup.prototype;
    if (sup.prototype.constructor !== sup) {
        sup.prototype.constructor = sup;
    }
}

應用:

function Ball(_a) {
    this.sup.constructor.call(this, _a);
}

Ball.prototype.play = function () {
    this.sup.play.call(this);
    console.log("end");
}

Ball.extend(Box);

let b7 = new Ball(10);
console.log(b7);

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • day19_JS_繼承進階 1.JS中的繼承 繼承是面向?qū)ο笾幸粋€比較核心的概念。其他正統(tǒng)面向?qū)ο笳Z言都會有兩種方...
    learninginto閱讀 279評論 0 9
  • 繼承的概念:子類可以使用父類共享的屬性和方法,避免重復代碼提高代碼復用性。 原型鏈:子類可以共享父類的實例對象和實...
    淺秋_6672閱讀 462評論 0 0
  • 前言 上篇文章詳細解析了原型、原型鏈的相關知識點,這篇文章講的是和原型鏈有密切關聯(lián)的繼承,它是前端基礎中很重要的一...
    OBKoro1閱讀 1,355評論 0 0
  • ??面向?qū)ο螅∣bject-Oriented,OO)的語言有一個標志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意...
    霜天曉閱讀 2,256評論 0 6
  • HELLO大家好,我是你們喜歡的賣漢服的小姐姐??隙ù蠹叶冀?jīng)常會看抖音吧,畢竟是現(xiàn)在最火的短視頻APP,抖音里的小...
    漢愫漢服閱讀 763評論 0 0

友情鏈接更多精彩內(nèi)容