詳解Javascript繼承的實現(xiàn)

字號:


    這篇文章主要介紹了詳解Javascript繼承的實現(xiàn)的相關(guān)資料,需要的朋友可以參考下
    本文從以下四個方面展開話題:
    •1. 混合方式實現(xiàn)及問題
    •2. 期望的調(diào)用方式
    •3. 繼承庫的詳細實現(xiàn)
    •4. 總結(jié)
    感興趣的朋友可以繼續(xù)往下閱讀詳情。
    我最早掌握的在js中實現(xiàn)繼承的方法是在xx學(xué)到的混合原型鏈和對象冒充的方法,在工作中,只要用到繼承的時候,我都是用這個方法實現(xiàn)。它的實現(xiàn)簡單,思路清晰:用對象冒充繼承父類構(gòu)造函數(shù)的屬性,用原型鏈繼承父類prototype 對象的方法,滿足我遇到過的所有繼承的場景。正因如此,我從沒想過下次寫繼承的時候,我要換一種方式來寫,直到今天晚上看了三生石上關(guān)于javascript繼承系列的文章(出的很早,現(xiàn)在才看,真有點可惜),才發(fā)現(xiàn)在js里面,繼承機制也可以寫的如此貼近java這種后端語言的實現(xiàn),確實很妙!所以我想在充分理解他博客的思路下,實現(xiàn)一個自己今后用得到的一個繼承庫。
    1. 混合方式實現(xiàn)及問題
    了解問題之前,先看看它的具體實現(xiàn):
    - Hide code
    //父類構(gòu)造函數(shù)
    function Employee(name, salary) {
    //實例屬性:姓名
    this.name = name;
    //實例屬性:薪資
    this.salary = salary;
    }
    //通過字面量對象設(shè)置父類的原型,給父類添加實例方法
    Employee.prototype = {
    //由于此處添加實例方法時也是通過修改父類原型處理的,
    //所以必須修改父類原型的constructor指向,避免父類實例的constructor屬性指向Object函數(shù)
    constructor: Employee,
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    }
    //子類構(gòu)造函數(shù)
    function Manager(name, salary, percentage) {
    //對象冒充,實現(xiàn)屬性繼承(name, salary)
    Employee.apply(this, [name, salary]);
    //實例屬性:提成
    this.percentage = percentage;
    }
    //將父類的一個實例設(shè)置為子類的原型,實現(xiàn)方法繼承
    Manager.prototype = new Employee();
    //修改子類原型的constructor指向,避免子類實例的constructor屬性指向父類的構(gòu)造函數(shù)
    Manager.prototype.constructor = Manager;
    //給子類添加新的實例方法
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    }
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(m instanceof Manager); //true
    console.log(m instanceof Employee); //true
    console.log(e instanceof Employee); //true
    console.log(e instanceof Manager); //false
    從結(jié)果上來說,這種繼承實現(xiàn)方式?jīng)]有問題,Manager的實例同時繼承到了Employee類的實例屬性和實例方法,并且通過instanceOf運算的結(jié)果也都正確。但是從代碼組織和實現(xiàn)細節(jié)層面,這種方法還有以下幾個問題: 
    1)代碼組織不夠優(yōu)雅,繼承實現(xiàn)的關(guān)鍵部分的邏輯是通用的,都是如下結(jié)構(gòu):
    - Hide code
    //將父類的一個實例設(shè)置為子類的原型,實現(xiàn)方法繼承
    SubClass.prototype = new SuperClass();
    //修改子類原型的constructor指向,避免子類實例的constructor屬性指向父類的構(gòu)造函數(shù)
    SubClass.prototype.constructor = SubClass;
    //給子類添加新的實例方法
    SubClass.prototype.method1 = function() {
    }
    SubClass.prototype.method2 = function() {
    }
    SubClass.prototype.method3 = function() {
    }
    這段代碼缺乏封裝。另外在添加子類的實例方法時,不能通過SubClass.prototype = { method1: function() {} }這種方式去設(shè)置,否則就把子類的原型整個又修改了,繼承就無法實現(xiàn)了,這樣每次都得按SubClass.prototype.method1 = function() {} 的結(jié)構(gòu)去寫,代碼看起來很不連續(xù)。
    解決方式:利用模塊化的方式,將通用的邏輯封裝起來,對外提供簡單的接口,只要按照約定的接口調(diào)用,就能夠簡化類的構(gòu)建與類的繼承。具體實現(xiàn)請看后面的內(nèi)容介紹,暫時只能提供理論的說明。
    2)在給子類的原型設(shè)置成父類的實例時,調(diào)用的是new SuperClass(),這是對父類構(gòu)造函數(shù)的無參調(diào)用,那么就要求父類必須有無參的構(gòu)造函數(shù)??墒窃趈avascript中,函數(shù)無法重載,所以父類不可能提供多個構(gòu)造函數(shù),在實際業(yè)務(wù)中,大部分場景下父類構(gòu)造函數(shù)又不可能沒有參數(shù),為了在唯一的一個構(gòu)造函數(shù)中模擬函數(shù)重載,只能借助判斷arguments.length來處理。問題就是,有時候很難保證每次寫父類構(gòu)造函數(shù)的時候都會添加arguments.length的判斷邏輯。這樣的話,這個處理方式就是有風(fēng)險的。要是能把構(gòu)造函數(shù)里的邏輯抽離出來,讓類的構(gòu)造函數(shù)全部是無參函數(shù)的話,這個問題就很好解決了。
    解決方式:把父類跟子類的構(gòu)造函數(shù)全部無參化,并且在構(gòu)造函數(shù)內(nèi)不寫任何邏輯,把構(gòu)造函數(shù)的邏輯都遷移到init這個實例方法,比如前面給出的Employee和Manager的例子就能改造成下面這個樣子:
    - Hide code
    //無參無邏輯的父類構(gòu)造函數(shù)
    function Employee() {}
    Employee.prototype = {
    constructor: Employee,
    //把構(gòu)造邏輯搬到init方法中來
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    };
    //無參無邏輯的子類構(gòu)造函數(shù)
    function Manager() {}
    Manager.prototype = new Employee();
    Manager.prototype.constructor = Manager;
    //把構(gòu)造邏輯搬到init方法中來
    Manager.prototype.init = function (name, salary, percentage) {
    //借用父類的init方法,實現(xiàn)屬性繼承(name, salary)
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
    };
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    };
    用init方法來完成構(gòu)造功能,就可以保證在設(shè)置子類原型時(Manager.prototype = new Employee()),父類的實例化操作一定不會出錯,唯一不好的是在調(diào)用類的構(gòu)造函數(shù)來初始化實例的時候,必須在調(diào)用構(gòu)造函數(shù)后手動調(diào)用init方法來完成實際的構(gòu)造邏輯:
    - Hide code
    var e = new Employee();
    e.init('jason', 5000);
    var m = new Manager();
    m.init('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(m instanceof Manager); //true
    console.log(m instanceof Employee); //true
    console.log(e instanceof Employee); //true
    console.log(e instanceof Manager); //false
    要是能把這個init的邏輯放在構(gòu)造函數(shù)內(nèi)部就好了,可是這樣的話就會違背前面說的構(gòu)造函數(shù)無參無邏輯的原則。換一種方式來考慮,這個原則的目的是為了保證在實例化父類作為子類原型的時候,調(diào)用父類的構(gòu)造函數(shù)不會出錯,那么就可以稍微打破一下這個原則,在類的構(gòu)造函數(shù)里添加少量的并且一定不會有問題的邏輯來解決:
    - Hide code
    //添加一個全局標(biāo)識initializing,表示是否正在進行子類的構(gòu)建和類的繼承
    var initializing = false;
    //可自動調(diào)用init方法的父類構(gòu)造函數(shù)
    function Employee() {
    if (!initializing) {
    this.init.apply(this, arguments);
    }
    }
    Employee.prototype = {
    constructor: Employee,
    //把構(gòu)造邏輯搬到init方法中來
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    };
    //可自動調(diào)用init方法的子類構(gòu)造函數(shù)
    function Manager() {
    if (!initializing) {
    this.init.apply(this, arguments);
    }
    }
    //表示開始子類的構(gòu)建和類的繼承
    initializing = true;
    //此時調(diào)用new Emplyee(),并不會調(diào)用Employee.prototype.init方法
    Manager.prototype = new Employee();
    Manager.prototype.constructor = Manager;
    //表示結(jié)束子類的構(gòu)建和類的繼承,之后調(diào)用new Employee或new Manager都會自動調(diào)用init實例方法
    initializing = false;
    //把構(gòu)造邏輯搬到init方法中來
    Manager.prototype.init = function (name, salary, percentage) {
    //借用父類的init方法,實現(xiàn)屬性繼承(name, salary)
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
    };
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    };
    調(diào)用結(jié)果仍然和前面的例子一樣。但是這個實現(xiàn)還有一個小問題,它引入了一個全局變量initializing,要是能把引入這個全局變量就好了,這個其實很好解決,只要我們把關(guān)于類的構(gòu)建跟繼承,封裝成一個模塊,然后把這個變量放在模塊的內(nèi)部,就沒有問題了。
    3)在構(gòu)造子類的時候,是把子類的原型設(shè)置成了父類的一個實例,這個是不符合語義的,繼承應(yīng)該發(fā)生在類與類之間,而不是類與實例之間。之所以要用父類的一個實例來作為子類的原型:
    - Hide code
    SubClass.prototype = new SuperClass();
    完全是因為父類的這個實例,指向父類的原型,而子類的實例又會指向子類的原型,所以最終子類的實例就能通過原型鏈訪問到父類原型上的方法。這個做法雖然能實現(xiàn)實例方法的繼承,但是它不符合語義,而且它還有一個很大的問題就是會增加原型鏈的長度,導(dǎo)致子類在調(diào)用父類方法時,必須通過原型鏈的查找到父類的方法才行。要是繼承層次較深,會對js的執(zhí)行性能有些影響。
    解決方式:在解決這個問題之前,先想想繼承能幫我們解決什么問題:從父類復(fù)用已有的實例屬性和實例方法。在javascript面向?qū)ο缶幊讨校恢庇幸粋€原則就是,實例屬性都寫在構(gòu)造函數(shù)或者實例方法里面,實例方法寫在原型上面,也就是說類的原型,按照這個原則來說,就是用來寫實例方法的,而且是只用來寫實例方法,那么我們完全可以在構(gòu)建子類時,通過復(fù)制的方式將父類原型的所有方法全部添加到子類的原型上,不一定要把父類的一個實例設(shè)置成子類的原型,這樣就能將原型鏈的長度大大地縮短,借助一個簡短的copy函數(shù),我們就能輕松對前面的代碼進行改造:
    - Hide code
    //用來復(fù)制父類原型,由于父類原型上約定只寫實例方法,所以復(fù)制的時候不必擔(dān)心引用的問題
    var copy = function (source) {
    var target = {};
    for (var i in source) {
    if (source.hasOwnProperty(i)) {
    target[i] = source[i];
    }
    }
    return target;
    }
    function Employee() {
    this.init.apply(this, arguments);
    }
    Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    };
    function Manager() {
    this.init.apply(this, arguments);
    }
    //將父類的原型方法復(fù)制到子類的原型上
    Manager.prototype = copy(Employee.prototype);
    //子類還是需要修改constructor指向,因為從父類原型復(fù)制出來的對象的constructor還是指向父類的構(gòu)造函數(shù)
    Manager.prototype.constructor = Manager;
    Manager.prototype.init = function (name, salary, percentage) {
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
    };
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    };
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(m instanceof Manager); //true
    console.log(m instanceof Employee); //false
    console.log(e instanceof Employee); //true
    console.log(e instanceof Manager); //false
    這么做了以后,當(dāng)調(diào)用m.toString的時候其實調(diào)用的是Manager類自身原型上的方法,而不是Employee類的實例方法,縮短了在原型鏈上查找方法的距離。這個做法在性能上有很大的優(yōu)點,但不好的是通過原型鏈維持的繼承關(guān)系其實已經(jīng)斷了,子類的原型和子類的實例都無法再通過js原生的屬性訪問到父類的原型,所以這個調(diào)用console.log(m instanceof Employee)輸出的是false。不過跟性能比起來,這個都可以不算問題:一是instanceOf的運算,幾乎在javascript的開發(fā)里面用不到,至少我是沒碰到過;二是通過復(fù)制方式完全能夠把父類的實例方法繼承下來,這就已經(jīng)達到了繼承的最大目的。
    這個方法還有一個額外的好處是,解決了第2個問題最后提到的引入initializing全局變量的問題,如果是復(fù)制的話,就不需要在構(gòu)建繼承關(guān)系時,去調(diào)用父類的構(gòu)造函數(shù),那么也就沒有必要在構(gòu)造函數(shù)內(nèi)先判斷initializing才能去調(diào)用init方法,上面的代碼中就已經(jīng)去掉了initializing這個變量的處理。
    4)在子類的構(gòu)造函數(shù)和實例方法內(nèi)如果想要調(diào)用父類的構(gòu)造函數(shù)或者方法,顯得比較繁瑣:
    - Hide code
    function SuperClass() {}
    SuperClass.prototype = {
    constructor: SuperClass,
    method1: function () {}
    }
    function SubClass() {
    //調(diào)用父類構(gòu)造函數(shù)
    SuperClass.apply(this);
    }
    SubClass.prototype = new SuperClass();
    SubClass.prototype.constructor = SubClass;
    SubClass.prototype.method1 = function () {
    //調(diào)用父類的實例方法
    SuperClass.prototype.method1.apply(this, arguments);
    }
    SubClass.prototype.method2 = function () {}
    SubClass.prototype.method3 = function () {}
    每次都得靠apply借用方法來處理。要是能改成如下的調(diào)用就好用多了:
    - Hide code
    function SubClass() {
    //調(diào)用父類構(gòu)造函數(shù)
    this.base();
    }
    SubClass.prototype = new SuperClass();
    SubClass.prototype.constructor = SubClass;
    SubClass.prototype.method1 = function() {
    //調(diào)用父類的實例方法
    this.base();
    }
    解決方式:如果要在每個實例方法里,都能通過this.base()調(diào)用父類原型上相應(yīng)的方法,那么this.base就一定不是一個固定的方法,需要在每個實例方法執(zhí)行期間動態(tài)地將this.base指定為父類原型的同名方法,能夠做到這個實現(xiàn)的方式,就只有通過方法代理了,前面的Employee和Manager的例子可以改造如下:
    - Hide code
    //用來復(fù)制父類原型,由于父類原型上約定只寫實例方法,所以復(fù)制的時候不必擔(dān)心引用的問題
    var copy = function (source) {
    var target = {};
    for (var i in source) {
    if (source.hasOwnProperty(i)) {
    target[i] = source[i];
    }
    }
    return target;
    };
    function Employee() {
    this.init.apply(this, arguments);
    }
    Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    };
    function Manager() {
    //必須在每個實例中添加baseProto屬性,以便實例內(nèi)部可以通過這個屬性訪問到父類的原型
    //因為copy函數(shù)導(dǎo)致原型鏈斷裂,無法通過原型鏈訪問到父類的原型
    this.baseProto = Employee.prototype;
    this.init.apply(this, arguments);
    }
    Manager.prototype = copy(Employee.prototype);
    //子類還是需要修改constructor指向,因為從父類原型復(fù)制出來的對象的constructor還是指向父類的構(gòu)造函數(shù)
    Manager.prototype.constructor = Manager;
    Manager.prototype.init = (function (name, func) {
    return function () {
    //記錄實例原有的this.base的值
    var old = this.base;
    //將實例的this.base指向父類的原型的同名方法
    this.base = this.baseProto[name];
    //調(diào)用子類自身定義的init方法,也就是func參數(shù)傳遞進來的函數(shù)
    var ret = func.apply(this, arguments);
    //還原實例原有的this.base的值
    this.base = old;
    return ret;
    }
    })('init', function (name, salary, percentage) {
    //通過this.base調(diào)用父類的init方法
    //這個函數(shù)真實的調(diào)用位置是var ret = func.apply(this, arguments);
    //當(dāng)調(diào)用Manager實例的init方法時,其實不是調(diào)用的這個函數(shù)
    //而是調(diào)用上面那個匿名函數(shù)里面return的匿名函數(shù)
    //在return的匿名函數(shù)里,先把this.base指向為了父類原型的同名函數(shù),然后在調(diào)用func
    //func內(nèi)部再通過調(diào)用this.base時,就能調(diào)用父類的原型方法。
    this.base(name, salary);
    this.percentage = percentage;
    });
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    };
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(m instanceof Manager); //true
    console.log(m instanceof Employee); //false
    console.log(e instanceof Employee); //true
    console.log(e instanceof Manager); //false
    通過代理的方式,就解決了在在實例方法內(nèi)部通過this.base調(diào)用父類原型同名方法的問題??墒窃趯嶋H情況中,每個實例方法都有可能需要調(diào)用父類的實例,那么每個實例方法都要添加同樣的代碼,顯然這會增加很多麻煩,好在這部分的邏輯也是同樣的,我們可以把它抽象一下,最后都放到模塊化的內(nèi)部去,這樣就能簡化代理的工作。
    5)未考慮靜態(tài)屬性和靜態(tài)方法。盡管靜態(tài)成員是不需要繼承的,但在有些場景下,我們還是需要靜態(tài)成員,所以得考慮靜態(tài)成員應(yīng)該添加在哪里。
    解決方式:由于js原生并不支持靜態(tài)成員,所以只能借助一些公共的位置來處理。最佳的位置是添加到構(gòu)造函數(shù)上:
    - Hide code
    var copy = function (source) {
    var target = {};
    for (var i in source) {
    if (source.hasOwnProperty(i)) {
    target[i] = source[i];
    }
    }
    return target;
    };
    function Employee() {
    this.init.apply(this, arguments);
    }
    //添加一個靜態(tài)屬性
    Employee.idCounter = 1;
    //添加一個靜態(tài)方法
    Employee.getId = function () {
    return Employee.idCounter++;
    };
    Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    //調(diào)用靜態(tài)方法
    this.id = Employee.getId();
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    };
    function Manager() {
    this.baseProto = Employee.prototype;
    this.init.apply(this, arguments);
    }
    Manager.prototype = copy(Employee.prototype);
    Manager.prototype.constructor = Manager;
    Manager.prototype.init = (function (name, func) {
    return function () {
    var old = this.base;
    this.base = this.baseProto[name];
    var ret = func.apply(this, arguments);
    this.base = old;
    return ret;
    }
    })('init', function (name, salary, percentage) {
    this.base(name, salary);
    this.percentage = percentage;
    });
    Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
    };
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(m instanceof Manager); //true
    console.log(m instanceof Employee); //false
    console.log(e instanceof Employee); //true
    console.log(e instanceof Manager); //false
    console.log(m.id); //2
    console.log(e.id); //1
    最后的兩行輸出了正確的實例id,而這個id是通過Employee類的靜態(tài)方法生成的。在java的面向?qū)ο缶幊讨?,子類跟父類都可以定義靜態(tài)成員,在調(diào)用的時候還存在覆蓋的問題,在js里面,因為受語言的限制,自定義的靜態(tài)成員不可能實現(xiàn)全面的面向?qū)ο蠊δ?,就像上面這種,能夠給類提供一些公共的屬性和公共方法,就已經(jīng)足夠了。
    2. 期望的調(diào)用方式
    從第1部分的分析可以看出,在js里面,類的構(gòu)建與繼承,有很多通用的邏輯,完全可以把這些邏輯封裝成一個單獨的模塊,形成一個通用的類庫,以便在工作中有需要的時候,都可以直接拿來使用。這個類庫要求能完成我們需要的功能(類的構(gòu)建與繼承和靜態(tài)成員的添加),同時在使用時要足夠簡潔方便。在利用bootstrap的modal組件自定義alert,confirm和modal對話框這篇文章里,我曾說過一些從組件期望的調(diào)用方式,去反推組件實現(xiàn)的一些觀點,當(dāng)你明確你需要什么東西時,你才知道這個東西你該怎么去創(chuàng)造。本文要編寫的這個繼承組件也會采取這個方法來實現(xiàn),我先用前面Employee和Manager的例子來模擬調(diào)用這個繼承庫的場景,通過預(yù)設(shè)的一些組件名稱或者接口名稱以及調(diào)用方式,來嘗試走通真實使用這個繼承庫的流程,有了這個東西,下一步我只需要根據(jù)這個要求去實現(xiàn)即可,模擬如下:
    - Hide code
    //通過調(diào)用Class函數(shù)構(gòu)造一個類
    var Employee = Class({
    //通過instanceMembers指定這個類的實例成員
    instanceMembers: {
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    //調(diào)用靜態(tài)方法
    this.id = Employee.getId();
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    },
    //通過staticMembers指定這個類的靜態(tài)成員
    //靜態(tài)方法內(nèi)部可通過this訪問其它靜態(tài)成員
    //在外部可通過Employee.getId這種方式訪問到靜態(tài)成員
    staticMembers: {
    idCounter: 1,
    getId: function () {
    return this.idCounter++;
    }
    }
    });
    var Manager = Class({
    instanceMembers: {
    init: function (name, salary, percentage) {
    this.base(name, salary);
    this.percentage = percentage;
    Manager.count++;
    },
    getSalary: function () {
    return this.salary + this.salary * this.percentage;
    }
    },
    //通過extend指定要繼承的類
    extend: Employee
    });
    從模擬的結(jié)果來看,我想要的繼承庫對外提供的名稱只有Class, instanceMembers, staticMembers和extend而已,調(diào)用方式也很簡單,只要傳遞參數(shù)給Class函數(shù)即可。接下來就按照這個目標(biāo),看看如何一步步根據(jù)第一部分羅列的那些問題和解決方式,把這個庫給寫出來。
    3. 繼承庫的詳細實現(xiàn)
    根據(jù)API名稱和接口以及前面第1部分提出的問題,這個繼承庫要完成的功能有: 
    1)類的構(gòu)建(關(guān)鍵:init方法)和靜態(tài)成員處理; 
    2)繼承關(guān)系的構(gòu)建(關(guān)鍵:父類原型的復(fù)制); 
    3)父類方法的簡化調(diào)用(關(guān)鍵:父類原型上同名方法的代理)。 
    所以這個庫的實現(xiàn),可以按照這三點分成三版來開發(fā)。
    1)第一版
    在第一版里面,僅需要實現(xiàn)類的構(gòu)架和靜態(tài)成員添加的功能即可,細節(jié)如下:
    - Hide code
    var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;
    //用來判斷是否為Object的實例
    function isObject(o) {
    return typeof (o) === 'object';
    }
    //用來判斷是否為Function的實例
    function isFunction(f) {
    return typeof (f) === 'function';
    }
    function ClassBuilder(options) {
    if (!isObject(options)) {
    throw new Error('Class options must be an valid object instance!');
    }
    var instanceMembers = isObject(options) && options.instanceMembers || {},
    staticMembers = isObject(options) && options.staticMembers || {},
    extend = isObject(options) && isFunction(options.extend) && options.extend,
    prop;
    //表示要構(gòu)建的類的構(gòu)造函數(shù)
    function TargetClass() {
    if (isFunction(this.init)) {
    this.init.apply(this, arguments);
    }
    }
    //添加靜態(tài)成員,這段代碼需在原型設(shè)置的前面執(zhí)行,避免staticMembers中包含prototype屬性,覆蓋類的原型
    for (prop in staticMembers) {
    if (hasOwn.call(staticMembers, prop)) {
    TargetClass[prop] = staticMembers[prop];
    }
    }
    TargetClass.prototype = instanceMembers;
    TargetClass.prototype.constructor = TargetClass;
    return TargetClass;
    }
    return ClassBuilder
    })();
    這一版核心代碼在于類的構(gòu)建和靜態(tài)成員添加的部分,其它代碼僅僅提供一些提前可以想到的賦值函數(shù)和變量(isObject, isFunction),并做一些參數(shù)合法性校驗的處理。添加靜態(tài)成員的代碼一定要在設(shè)置原型的代碼之前,否則就有原型被覆蓋的風(fēng)險。有了這個版本,就可以直接構(gòu)建帶靜態(tài)成員的Employee類了:
    - Hide code
    var Employee = Class({
    instanceMembers: {
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    //調(diào)用靜態(tài)方法
    this.id = Employee.getId();
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    },
    staticMembers: {
    idCounter: 1,
    getId: function () {
    return this.idCounter++;
    }
    }
    });
    var e = new Employee('jason', 5000);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(e.id); //1
    console.log(e.constructor === Employee); //true
    在getId方法中之所以直接使用this就能訪問到構(gòu)造函數(shù)Employee,是因為getId這個方法是添加到構(gòu)造函數(shù)上的,所以當(dāng)調(diào)用Employee.getId()時,getId方法里面的this指向的就是Employee這個函數(shù)對象。
    第二版在第一版的基礎(chǔ)上,實現(xiàn)繼承關(guān)系的構(gòu)建部分:
    - Hide code
    var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;
    //用來判斷是否為Object的實例
    function isObject(o) {
    return typeof (o) === 'object';
    }
    //用來判斷是否為Function的實例
    function isFunction(f) {
    return typeof (f) === 'function';
    }
    //簡單復(fù)制
    function copy(source) {
    var target = {};
    for (var i in source) {
    if (hasOwn.call(source, i)) {
    target[i] = source[i];
    }
    }
    return target;
    }
    function ClassBuilder(options) {
    if (!isObject(options)) {
    throw new Error('Class options must be an valid object instance!');
    }
    var instanceMembers = isObject(options) && options.instanceMembers || {},
    staticMembers = isObject(options) && options.staticMembers || {},
    extend = isObject(options) && isFunction(options.extend) && options.extend,
    prop;
    //表示要構(gòu)建的類的構(gòu)造函數(shù)
    function TargetClass() {
    if (extend) {
    //如果有要繼承的父類
    //就在每個實例中添加baseProto屬性,以便實例內(nèi)部可以通過這個屬性訪問到父類的原型
    //因為copy函數(shù)導(dǎo)致原型鏈斷裂,無法通過原型鏈訪問到父類的原型
    this.baseProto = extend.prototype;
    }
    if (isFunction(this.init)) {
    this.init.apply(this, arguments);
    }
    }
    //添加靜態(tài)成員,這段代碼需在原型設(shè)置的前面執(zhí)行,避免staticMembers中包含prototype屬性,覆蓋類的原型
    for (prop in staticMembers) {
    if (hasOwn.call(staticMembers, prop)) {
    TargetClass[prop] = staticMembers[prop];
    }
    }
    //如果有要繼承的父類,先把父類的實例方法都復(fù)制過來
    extend && (TargetClass.prototype = copy(extend.prototype));
    //添加實例方法
    for (prop in instanceMembers) {
    if (hasOwn.call(instanceMembers, prop)) {
    TargetClass.prototype[prop] = instanceMembers[prop];
    }
    }
    TargetClass.prototype.constructor = TargetClass;
    return TargetClass;
    }
    return ClassBuilder
    })();
    這一版關(guān)鍵的部分在于:
    this.baseProto主要目的就是為了讓子類的實例能夠有一個屬性可以訪問到父類的原型,因為后面的繼承方式是復(fù)制方式,會導(dǎo)致原型鏈斷裂。有了這一版之后,就可以加入Manager類來演示效果了:
    - Hide code
    var Employee = Class({
    instanceMembers: {
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    //調(diào)用靜態(tài)方法
    this.id = Employee.getId();
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    },
    staticMembers: {
    idCounter: 1,
    getId: function () {
    return this.idCounter++;
    }
    }
    });
    var Manager = Class({
    instanceMembers: {
    init: function (name, salary, percentage) {
    //借用父類的init方法,實現(xiàn)屬性繼承(name, salary)
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
    },
    getSalary: function () {
    return this.salary + this.salary * this.percentage;
    }
    },
    extend: Employee
    });
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(e.constructor === Employee); //true
    console.log(m.constructor === Manager); //true
    console.log(e.id); //1
    console.log(m.id); //2
    不過在Manager內(nèi)部,調(diào)用父類的方法時還是apply借用的方式,所以在最后一版里面,需要把它變成我們期望的this.base的方式,反正原理前面也已經(jīng)了解了,無非是在方法同名的時候,對實例方法加一個代理而已,實現(xiàn)如下:
    - Hide code
    var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;
    //用來判斷是否為Object的實例
    function isObject(o) {
    return typeof (o) === 'object';
    }
    //用來判斷是否為Function的實例
    function isFunction(f) {
    return typeof (f) === 'function';
    }
    //簡單復(fù)制
    function copy(source) {
    var target = {};
    for (var i in source) {
    if (hasOwn.call(source, i)) {
    target[i] = source[i];
    }
    }
    return target;
    }
    function ClassBuilder(options) {
    if (!isObject(options)) {
    throw new Error('Class options must be an valid object instance!');
    }
    var instanceMembers = isObject(options) && options.instanceMembers || {},
    staticMembers = isObject(options) && options.staticMembers || {},
    extend = isObject(options) && isFunction(options.extend) && options.extend,
    prop;
    //表示要構(gòu)建的類的構(gòu)造函數(shù)
    function TargetClass() {
    if (extend) {
    //如果有要繼承的父類
    //就在每個實例中添加baseProto屬性,以便實例內(nèi)部可以通過這個屬性訪問到父類的原型
    //因為copy函數(shù)導(dǎo)致原型鏈斷裂,無法通過原型鏈訪問到父類的原型
    this.baseProto = extend.prototype;
    }
    if (isFunction(this.init)) {
    this.init.apply(this, arguments);
    }
    }
    //添加靜態(tài)成員,這段代碼需在原型設(shè)置的前面執(zhí)行,避免staticMembers中包含prototype屬性,覆蓋類的原型
    for (prop in staticMembers) {
    if (hasOwn.call(staticMembers, prop)) {
    TargetClass[prop] = staticMembers[prop];
    }
    }
    //如果有要繼承的父類,先把父類的實例方法都復(fù)制過來
    extend && (TargetClass.prototype = copy(extend.prototype));
    //添加實例方法
    for (prop in instanceMembers) {
    if (hasOwn.call(instanceMembers, prop)) {
    //如果有要繼承的父類,且在父類的原型上存在當(dāng)前實例方法同名的方法
    if (extend && isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
    TargetClass.prototype[prop] = (function (name, func) {
    return function () {
    //記錄實例原有的this.base的值
    var old = this.base;
    //將實例的this.base指向父類的原型的同名方法
    this.base = this.baseProto[name];
    //調(diào)用子類自身定義的實例方法,也就是func參數(shù)傳遞進來的函數(shù)
    var ret = func.apply(this, arguments);
    //還原實例原有的this.base的值
    this.base = old;
    return ret;
    }
    })(prop, instanceMembers[prop]);
    } else {
    TargetClass.prototype[prop] = instanceMembers[prop];
    }
    }
    }
    TargetClass.prototype.constructor = TargetClass;
    return TargetClass;
    }
    return ClassBuilder
    })();
    核心部分是:
    只有當(dāng)需要繼承父類,且父類原型中有方法與當(dāng)前的實例方法同名時,才會去對當(dāng)前的實例方法添加代理。更詳細的原理可以回到文章第1部分回顧相關(guān)內(nèi)容。至此,我們在Manager類內(nèi)部調(diào)用父類的方法時,就很簡單了,只要通過this.base即可:
    - Hide code
    var Employee = Class({
    instanceMembers: {
    init: function (name, salary) {
    this.name = name;
    this.salary = salary;
    //調(diào)用靜態(tài)方法
    this.id = Employee.getId();
    },
    getName: function () {
    return this.name;
    },
    getSalary: function () {
    return this.salary;
    },
    toString: function () {
    return this.name + '\'s salary is ' + this.getSalary() + '.';
    }
    },
    staticMembers: {
    idCounter: 1,
    getId: function () {
    return this.idCounter++;
    }
    }
    });
    var Manager = Class({
    instanceMembers: {
    init: function (name, salary, percentage) {
    //通過this.base調(diào)用父類的構(gòu)造方法
    this.base(name, salary);
    this.percentage = percentage;
    },
    getSalary: function () {
    return this.base() + this.salary * this.percentage;
    }
    },
    extend: Employee
    });
    var e = new Employee('jason', 5000);
    var m = new Manager('tom', 8000, 0.15);
    console.log(e.toString()); //jason's salary is 5000.
    console.log(m.toString()); //tom's salary is 9200.
    console.log(e.constructor === Employee); //true
    console.log(m.constructor === Manager); //true
    console.log(e.id); //1
    console.log(m.id); //2
    注意這兩處調(diào)用:
    以上就是本文要實現(xiàn)的繼承庫的全部細節(jié),其實它所做的事就是把本文第1部分提到的那些問題的解決方式和第二部分模擬的調(diào)用場景結(jié)合起來,封裝到一個模塊內(nèi)部而已,各個細節(jié)的原理只要理解了第1部分總結(jié)的那些解決方式就很掌握了。在最后一版的演示中,也能看到,本文實現(xiàn)的這個繼承庫,已經(jīng)完全滿足了模擬場景中的需求,今后有任何需要用到繼承的場景,完全可以拿最后一版的實現(xiàn)去開發(fā)。
    4. 總結(jié)
    本文在三生石上關(guān)于javascript繼承系列博客的思路指引下,實現(xiàn)了一個易用的繼承庫,使用它可以更像java語言構(gòu)建面向?qū)ο蟮念惡皖愔g的繼承關(guān)系,我可以預(yù)見在將來的工作,這個庫對我的代碼質(zhì)量和功能實現(xiàn)會起到很重要的作用,因為在開發(fā)中,繼承的編碼思想還是應(yīng)用的非常多,尤其是當(dāng)我們做項目做得多的時候,一方面肯定想把一些公共的東西寫成可重用的組件,另一方面又必須得滿足各個項目的個性要求,所以在寫組件的時候不能寫的太死,多寫接口,等到具體項目的時候再通過繼承等方式來擴展該項目獨有的功能,這樣寫出的組件才會更靈活穩(wěn)定??傊辛诉@個繼承庫,感覺以后寫的代碼都會開心好多~所以希望本文的內(nèi)容也能對你有同樣的一些幫助。如果確實有幫助,求點推薦:)
    有關(guān)javascript繼承的實現(xiàn)小編就給大家介紹這么多,希望對大家有所幫助!