1. 程式人生 > >JavaScript原型鏈和繼承

JavaScript原型鏈和繼承

eat 過渡 存在 es6 向上 reset 保護 foo 創建

1.概念

  JavaScript並不提供一個class的實現,在ES6中提供class關鍵字,但是這個只是一個語法糖,JavaScript仍然是基於原型的。JavaScript只有一種結構:對象。每個對象都有一個私有屬性:_proto_,這個屬性指向它構造函數的原型對象(property)。它的原型對象也有一個屬於自己的原型對象,這樣層層向上知道一個對象的屬性為null。根據定義null沒有原型,它是這個原型鏈中的最後一個環節。

  幾乎所有的JavaScript中的對象都是位於原型鏈頂端的Object的實例。

2.基於原型鏈的繼承

  JavaScript對象是動態的屬性“包”(指其自己的屬性)。JavaScript對象有一個指向原型對象的鏈。當視圖訪問一個對象的屬性時,它不僅僅在對象上搜尋,還會搜尋對象的原型,以及該對象原型的原型,依次層層向上搜索,直至找到一個名字匹配的屬性或者到達原型鏈的頂端。

  在ECMA標準中,someObject.[[Prototype]]符號是用於指向someObject的原型。從ES6開始[[Prototype]]可以通過Object.getPrototypeOf()和Object.setPrototype()訪問器來訪問。這個是JavaScript的非標準,但是很多瀏覽器都實現了__proto__。註意瀏覽器沒有實現對象的object.Property這樣的屬性,即沒有實現對象實例的Prototype屬性。

  但是它和構造函數func的prototype屬性不同。被構造函數創建的實例對象的[[prototype]]指向func的prototype屬性。Object.prototype屬性表示Object的原型對象。

  這裏我們舉一個例子,假設我們有一個對象o,它有自己的屬性a, b,o 的原型 o.__proto__有屬性 b 和 c, 最後, o.__proto__.__proto__ 是 null,JavaScript代碼如下:

    var o = {a: 1, b: 2};
    o.__proto__ = {b: 3, c: 4};
    console.log(Object.getPrototypeOf(o));
    console.log(o.__proto__);
    console.log(Object.getPrototypeOf(Object.getPrototypeOf(o)));
    console.log(o.__proto__.__proto__);
    console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(o))));
    console.log(o.__proto__.__proto__.__proto__);

輸出結果如下:

技術分享圖片

第一句:定義一個對象o,對象有屬性a,b

第二句:設置對象o的原型為一個新的對象{b: 3, c: 4}

第三句:使用ES6方法Object.getPrototypeOf獲取對象o的原型,輸出{b: 3, c: 4}

第四句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型,輸出{b: 3, c: 4}

第五句:使用ES6的方法Object.getPrototypeOf獲取對象o的原型的原型,是原型鏈頂端Object的實例

第六句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型的原型,是原型鏈頂端Object的事例

第七句:使用ES6的方法Object.getPrototypeOf獲取對象o的原型的原型的原型,是null

第八句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型的原型的原型,null


3.繼承方法
JavaScript沒有其他基於類的語言所定義的“方法”。在JavaScript裏,任何函數都可以添加到對象上作為對象的屬性。函數的繼承與其他的屬性繼承沒有任何區別,包括“屬性遮蔽”(這相當於其他語言的方法重寫)。
當繼承的函數被調用時,this指向的當前繼承的對象,而不是繼承的函數所在的原型對象。看下面的例子:

    var o = {
        a: 2,
        m: function () {
            return this.a + 1;
        }
    };
    // 當調用o.m()的時候,this指向了o
    console.log(o.m());

    // 創建一個對象p,p.__proto__是o,p是一個集成自o的對象
    var p = Object.create(o);
    // 下面兩句和上面的效果一樣
    //    var p = {};
    //    p.__proto__ = o;

    // 創建p自身的屬性a
    p.a = 4;
    // 調用p.m()函數時this指向了p,p繼承o的m函數此時this.a,即p.a指向p自身的屬性a,最後得到5
    console.log(p.m());

上面代碼中,調用p對象的m()方法時,m()方法中this.a指向p對象的a屬性,而不是它的父對象o的屬性a,有點類似英語語法中的“就近原則”,即先從自身屬性開始找,而不是它的原型對象。

4.__proto__和prototype的關系

上面提到“JavaScript中只有一種結構,就是對象”,在JavaScript任何對象歸根結底都是對象類型,他們都有對象的共同特點,即都有私有屬性__proto__,基本上所有的瀏覽器都實現了這個屬性,但是不建議在代碼中使用這個屬性,所以使用了一個比較怪異的名字__proto__,表示智能在內部使用,也叫隱式屬性,意思是一個隱藏起來的屬性。__proto__屬性指向構造當前對象的構造函數的原型,它保證了對象實例能夠訪問在構造函數原型中定義的所有屬性和方法。

JavaScript中方法這個對象除了和其他對象一樣有隱式屬性__proto__之外,還有自己特有的屬性prototype,這個屬性是一個指針,prototype指向原型對象,這個對象包含所有實例共享的屬性和方法,我們把prototype屬性叫做原型屬性。prototype指向的原型對象又有一個屬性constructor,這個屬性也是一個指針,指回原構造函數,即這個方法。

下面我們來看一張圖:

技術分享圖片

1.構造函數Foo()的原型屬性Foo.prototype指向了原型對象,在原型對象中有共有的方法,構造函數聲明的實例f1,f2都共享這個方法。

2.原型對象Foo.prototype保存著實例的共享的方法,它又有一個指針constructor,指回到構造函數,即函數Foo()。

3.實例。f1,f2是Foo這個對象的兩個實例,這兩個對象也有屬性__proto__,指向構造函數的原型對象,這樣就可以訪問原型對象的所有方法。

4.構造函數Foo()除了是方法,也是對象,它的__proto__屬性指向它的構造函數的原型對象Function.prototype。

5.Foo()的原型對象也是對象,它的__proto__屬性指向它的構造函數的原型對象,即Object.prototype。

6.最後Object.prototype的__proto__指向null。

7.對象有屬性__proto__,指向該對象的構造函數的原型對象。

8.方法除了有屬性__proto__,還有屬性prototype,指向該方法的原型對象。

4. 使用不同的方法來創建對象和生成原型鏈

4.1 語法結構創建的對象

var o = { a: 1 };這是一個定義對象的語法,這個語句使對象o繼承了Object.prototype上所有的屬性,o本身沒有名為hasOwenProperty的屬性,hasOwnProperty是Object.property的屬性,因此對象o繼承了Object.prototype的hasOwnProperty屬性方法。Object.property的原型為null,原型鏈如下:o -> Object.prototype -> null,截圖如下:

技術分享圖片

var a = ["yo", "whadup", "?"]; 這是一個定義數組的語法,數組都繼承於Array.prototype,Array.prototype中包含indexOf,forEach等方法,原型鏈如下:a -> Array.prototype -> Object.prototype -> null,截圖如下:

技術分享圖片

function f() = { return 2; } 這是一個定義函數的語法,函數都繼承於Function.prototype,Function.prototype中包含call,bind等方法,原型鏈如下:f -> Function.prototype -> Object.prototype -> null,使用console.log方法輸出f,console.log(f)只能把函數的內容輸出,並不能看到函數的原型,函數的原型的原型,目前本人還沒有搞清楚這個問題。截圖如下:

技術分享圖片

4.2使用構造器創建的對象

在JavaScript中,構造器其實就是一個普通的函數。當使用new操作符來作用這個函數時,它就可以被成為構造方法或者構造函數。看下面的代碼:

    function Graph() {
        this.vertices = []
        this.edges = []
    }

    Graph.prototype = {
        addVertice: function (v) {
            this.vertices.push(v);
        }
    }
    var g = new Graph();
    console.log(g);

輸出如下:

技術分享圖片

g是生成的對象,它有自己的屬性‘vertices’和‘edges’,在g被實例化時,g.[[Prototype]]指向了Graph.prototype

4.3 Object.create創建的對象

ECMAScript5中引入了一個新的方法:Object.create()。可以調用這個方法來創建一個新對象。新對象的原型就是調用create方法時傳入的第一個參數。我們來看下面的例子:輸出結果如下:

    var a = {a: 1};
    var b = Object.create(a);
    console.log(b.a);

    var c = Object.create(b);
    console.log(c);
    console.log(c.a);

    var d = Object.create(null);
    console.log(d.hasOwnProperty);

輸出結果如下:

技術分享圖片

第一句:定義對象a,它有屬性a

第二句:使用Object.Create(a)創建對象b,b的原型是a

第三句:輸出b.a,現在對象b上查找屬性a,沒有,然後在b的原型上找,值是1,輸出1

第四句:使用Object.Create(b)創建對象c,c的原型是b

第五句:輸出對象c,它的原型的原型上有一個屬性c,值為1

第六句:輸出c.a,現在對象c的屬性中查找a,沒有,在c的原型b上查找屬性a,沒有,在b的原型a上查找屬性a,有,值為1,輸出1

第七句:使用Object.Create(null)創建對象d,註意null沒有原型

第八句:輸出d.hasOwnProperty方法,在d的方法中找,沒有,在d的原型null中找,也沒有,最後輸出undefined

4.4 class關鍵字創建對象

es6引入一套新的關鍵字來實現class。使用基於類的語言對這些結構會很熟悉,但它們是不同的。JavaScript是基於原型的。這些新的關鍵字包括class,constructor,static,extends和super。來看下面的例子:

    class Polygon {
        constructor(height, width) {
            this.width = width;
            this.height = height;
        }
    }

    class Square extends Polygon {
        constructor(sideLength) {
            super(sideLength, sideLength);
        }

        get area() {
            return this.height * this.width;
        }

        set sideLength(sideLength) {
            this.height = sideLength;
            this.width = sideLength;
        }
    }

    var square = new Square(2);
    writeStr(square.area);

輸出結果如下:

技術分享圖片

5. JavaScript中的繼承

5.1 先看看如何封裝

上面我們講到創建對象的方式,有了對象之後就會有封裝,在JavaScript中封裝一個類很容易。通過構造器創建對象時,在構造函數(類)的內部通過對this(函數內部自帶的變量,用於指向當前這個對象)變臉添加屬性或者方法來實現添加屬性或方法。代碼如下:

    // 類的封裝
    function Book1 (id, bookname, price) {
        this.id = id;
        this.bookname = bookname
        this.price = price
    }
    var Book2 = function (id, bookname, price) {
        this.id = id;
        this.bookname = bookname
        this.price = price
    }

也可以通過在構造函數類(對象)的原型上添加屬性和方法。有兩種方式,一種是為原型對象賦值,另一種是講一個對象賦值給類的原型對象。如下:

    // 方式一
    Book.prototype.display = function () {

    }
    // 方式二
    Book.prototype = {
        display: function () {

        }
    }

需要訪問類的屬性和方法時不能直接使用Book類,例如Book.name,Book.display(),而要用new關鍵字來創建新的對象,然後通過點語法來訪問。

通過this添加的屬性,方法是在當前函數對象上添加的,JavaScript是一種基於原型prototype的語言,所以每次通過一個構造函數創建對象的時候,這個對象都有一個原型prototype指向其繼承的屬性,方法。所以通過prototype繼承來的屬性和方法不是對象自身的,但是在使用這些屬性和方法的時候需要通過prototype一級一級向上查找。

通過this定義的屬性或方法是該函數對象自身擁有的,每次通過這個函數創建新對象的時候this指向的屬性和方法都會相應的創建,而通過prototype繼承的屬性或者方法是通過prototype訪問到的,每次通過函數創建新對象時這些屬性和方法不會再次創建,也就是說只有單獨的一份。

面向對象概念中“私有屬性”,“私有方法”,“公有屬性”,“公有方法”,“保護方法”在JavaScript中又是怎麽實現的呢?

私有屬性,私有方法:由於JavaScript函數級作用域,聲明在函數內部的變量和方法在外界是訪問不到的,通過這個特性可以創建類的私有變量以及私有方法。

公有屬性,公有方法:在函數內部通過this創建的屬性和方法,在類創建對象時,沒有對象自身都擁有一份並且可以在外部訪問到,因此通過this創建的屬性,方法可以看做對象公有屬性和對象公有方法。類通過prototype創建的屬性或方法在實例的對象中通過點語法訪問到,所以可以將prototype對象中的屬性和方法也稱為類的公有屬性,類的公有方法。

特權方法:通過this創建的方法,不但可以訪問這些對象的共有屬性,方法,而且可以訪問到類或者對象自身的私有屬性和私有方法,權利比較大,所以可以看做是特權方法。

類構造器:在對象創建時可以通過特權方法實例化對象的一些屬性,因此這些在創建對象時調用的特權方法可以看做類的構造器。

靜態共有屬性,靜態共有方法:通過new關鍵字和方法名來創建新對象時,由於外面通過點語法添加的屬性和方法沒有執行到,所以新創建的對象中無法使用它們,但是可以通過類名來使用。因此在類外面通過點語法來創建的屬性,方法可以被稱為類的靜態共有屬性和類的靜態共有方法。

參考下面的代碼:

    var Book = function (id, name, price) {
        // 私有屬性
        var num = 1;
        // 私有方法
        function checkId() {
        };
        // 特權方法
        this.getName = function () {
        };
        this.getPrice = function () {
        };
        this.setName = function () {
        };
        this.setPrice = function () {
        };
        // 對象公有屬性
        this.id = id;
        // 對象公有方法
        this.copy = function () {
        };
        // 構造器
        this.setName(name);
        this.setPrice(price);
    }
    // 類靜態公有屬性(對象不能訪問)
    Book.isChinese = true;
    // 類靜態公有方法(對象不能訪問)
    Book.resetTime = function () {
        console.log(‘new Time‘);
    };
    Book.prototype = {
        // 公有屬性
        isJSBook: false,
        //公有方法
        display: function () {
        }
    };

通過new關鍵字創建對象是指是對新對象的this不斷的賦值,並將prototype指向類的prototype所指向的對象,而在類的構造函數外面通過點語法定義的屬性,方法不會添加在新的對象上。因此要想在新創建的對象上訪問isChinese就得通過Book類而不能通過this,如Book.isChinese,類的原型上定義的屬性在新對象裏可以直接使用,這是因為新對象的prototype和類的prototype指向同一個對象。

類的私有屬性num以及靜態公有屬性isChiese在新創建的對象裏是訪問不到的,而類的公有屬性isJSBook在對象中可以通過點語法訪問到。看下面實例代碼,註意這段代碼是在上面的實例代碼基礎上寫的:

    var b = new Book(11, ‘Javascript‘, 50);
    console.log(b.num); // undefined
    console.log(b.isJSBook); // false
    console.log(b.id); // 11
    console.log(b.isChinese); // undefined
    console.log(Book.isChinese); // true
    Book.resetTime(); // new Time

new關鍵字的作用可以看做對當前對象的this不停地賦值,如果沒有指定new關鍵字則this默認指向當前全局變量,一般是window,來看下面的代碼:

5.1 子類的原型對象—類式繼承

    // 類式繼承
    // 申明父類
    function SuperClass() {
        this.superValue = true
    }
    //為父類添加共有方法
    SuperClass.prototype.getSuperValue = function () {
        return this.superValue;
    }

    // 申明子類
    function SubClass() {
        this.subValue = false;
    }
    // 繼承父類
    SubClass.prototype= new SuperClass()
    // 為子類添加共有方法
    SubClass.prototype.getSubValue = function () {
        return this.subValue;
    }
    let sup = new SuperClass();
    let sub = new SubClass();
    console.log(sup.getSuperValue());       //true
    // console.log(sup.getSubValue());         //Uncaught TypeError: sup.getSubValue is not a function
    console.log(sub.getSubValue());         // false
    console.log(sub.getSuperValue());       // true
    console.log(sub instanceof SubClass);   // true
    console.log(sub instanceof SuperClass); // true
    console.log(sup instanceof SubClass);   // false
    console.log(sup instanceof SuperClass); // true
    console.log(SubClass.prototype instanceof SuperClass); // true
    console.log(SubClass.prototype instanceof SuperClass.prototype); // Uncaught TypeError: Right-hand side of ‘instanceof‘ is not callable
    console.log(sub.prototype instanceof SuperClass); // false

類的原型對象用來為類添加共有方法,但是不能直接添加,訪問這些屬性和方法,必須通過原型prototype來訪問。新創建的對象復制了父類構造函數的屬性和方法,並將原型__proto__指向父類的原型對象,這樣就擁有了父類的原型對象上的屬性和方法,這個新創建的對象可以直接訪問到父類原型對象上的屬性和方法。

這種繼承方式有2個缺點,其一,子類通過其原型對父類實例化,繼承了父類。如果父類中的共有屬性是引用類型的話,會被所有子類的實例公用,任何一個子類實例修改了父類屬性(引用類型),會直接影響到所有子類和這個父類。看下面代碼:

    function SuperClass() {
        this.books = [‘javascript‘, ‘html‘];
    }
    function SubClass() {}
    SubClass.prototype = new SuperClass();
    var instance1 = new SubClass();
    var instance2 = new SubClass();
    console.log(instance1.books); //["javascript", "html"]
    instance2.books.push(‘java‘);
    console.log(instance1.books); //["javascript", "html", "java"]
    console.log(SuperClass.books);//undefined
    var sup1 = new SuperClass();
    var sup2 = new SuperClass();
    sup2.books.push(‘css‘);
    console.log(sup1.books); // ["javascript", "html"]
    console.log(sup2.books); // ["javascript", "html", "css"]

上面例子中instance2修改了父類的books屬性,添加了一個“java”,結果instance1的books屬性也有了個新的元素“java”。註意SubClass.prototype = new SuperClass();new操作符會復制一份父類的屬性和方法,var sup = new SuperClass();也會復制一份父類的屬性和方法,但是他們是不同的,相互不會影響。並且只有前者才會出現這種引用類型被無意修改的情況。

5.2 創建即繼承—構造函數繼承

    // 構造函數繼承
    // 申明父類
    function SuperClass(id) {
        // 引用類型共有屬性
        this.books = [‘javascript‘, ‘html‘, ‘css‘];
        // 值型共有屬性
        this.id = id;
    }

    // 父類申明原型方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books);
    }

    // 申明子類
    function subClass(id) {
        // 繼承父類
        SuperClass.call(this, id);
    }
    // 創建兩個實例
    var instance1 = new subClass(10);
    var instance2 = new subClass(11);
    instance1.books.push(‘java‘);

    console.log(instance1.books); // ["javascript", "html", "css", "java"]
    console.log(instance1.id);    // 10
    console.log(instance2.books); // ["javascript", "html", "css"]
    console.log(instance2.id);    // 11
    instance1.showBooks();        // Uncaught TypeError: instance1.showBooks is not a function
    instance2.showBooks();        // Uncaught TypeError: instance1.showBooks is not a function

    // 申明父類實例
    var instance3 = new SuperClass(12);
    instance3.showBooks();      // ["javascript", "html", "css"]

註意SuperClass.call(this, id);這句是構造函數式繼承的關鍵。call方法可以改變函數的作用環境,在子類中對SuperClass調用合格方法就是講子類中的變量在父類中執行一遍,在父類中給this綁定屬性,因此子類就繼承了父類的共有屬性。由於這種類型的繼承沒有涉及原型,所以父類的原型方法不會被子類繼承,要想被子類繼承就必須放在構造函數中,這樣創建的實例會單獨擁有一份父類的屬性和方法,而不是共用,這樣違背了代碼復用的原則。

5.3 組合繼承

組合繼承又叫“偽經典繼承”,是指將原型鏈和構造函數技術組合在一起的一種繼承方式,下面看一個例子:

    // 申明父類
    function SuperClasss(name) {
        // 值類型共有屬性
        this.name = name;
        // 引用類型共有屬性
        this.books = [‘html‘, ‘css‘, ‘Javascript‘];
    }
    // 父類原型共有方法
    SuperClasss.prototype.getName = function () {
        console.log(this.name);
    }
    // 申明子類
    function SubClass(name, time) {
        // 構造函數式繼承父類name屬性
        SuperClasss.call(this, name);
        // 子類的共有屬性
        this.time = time;
    }
    // 類式繼承,子類原型繼承父類
    SubClass.prototype = new SuperClasss();
    // 子類原型方法
    SubClass.prototype.getTime = function () {
        console.log(this.time);
    }
    var instance1 = new SubClass(‘js book‘, 2014);
    instance1.books.push(‘java‘);
    console.log(instance1.books); // [‘html‘, ‘css‘, ‘Javascript‘, ‘java‘]
    instance1.getName(); // ‘js book‘
    instance1.getTime(); // 2014

    var instance2 = new SubClass(‘css book‘, 2013);
    console.log(instance2.books); // [‘html‘, ‘css‘, ‘Javascript‘]
    instance2.getName(); // ‘css book‘
    instance2.getTime(); // 2013

在子類構造函數中執行父類構造函數,在子類原型上實例化父類就是組合模式。子類的實例中更改父類繼承下來的引用類型屬性books不會影響到其他實例,並且子類實例化過程中又能將參數傳遞到父類的構造函數中。

這種方式也有缺點,在使用構造函數繼承時執行了一次父類的構造函數,而在實現子類原型的類式繼承時又調用了一遍父類的構造函數。

5.4 簡潔的繼承—原型式繼承

原型式繼承的思想是借助prototype根據已有的對象創建一個新的對象,同時不必創建新的自定義對象類型。代碼如下:

    // 原型式繼承
    function inheritObject(o) {
        // 申明一個過渡函數對象
        function F() {}
        // 過渡對象的原型繼承父對象
        F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
        return new F();
    }
    var book = {
        name: ‘js book‘,
        alikeBook: [‘css book‘, ‘html book‘]
    }
    var newBook = inheritObject(book);
    newBook.name = ‘ajax book‘;
    newBook.alikeBook.push(‘xml book‘);

    var otherBook = inheritObject(book)
    otherBook.name = ‘flash book‘;
    otherBook.alikeBook.push(‘as book‘);

    console.log(newBook.name); // ajax book
    console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    console.log(otherBook.name); // flash book
    console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    console.log(book.name); // js book
    console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]

和類式繼承一樣,父類對象book中的值類型被復制,引用類型屬性被共用。

5.5 寄生式繼承

    // 寄生式繼承
    function inheritObject(o) {
        // 申明一個過渡函數對象
        function F() {}
        // 過渡對象的原型繼承父對象
        F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
        return new F();
    }
    var book = {
        name: ‘js book‘,
        alikeBook: [‘css book‘, ‘html book‘]
    }
    function createBook(obj) {
        // 通過原型繼承方式創建對象
        var o = new inheritObject(obj);
        // 拓展對象
        o.getName = function () {
            console.log(obj.name);
        }
        // 返回拓展後的新對象
        return o;
    }
    var newBook = createBook(book);
    newBook.name = ‘ajax book‘;
    newBook.alikeBook.push(‘xml book‘);

    var otherBook = createBook(book);
    otherBook.name = ‘flash book‘;
    otherBook.alikeBook.push(‘as book‘);

    console.log(newBook.name); // ajax book
    newBook.getName(); // js book
    console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"]

    console.log(otherBook.name); // flash book
    console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"]
    otherBook.getName(); // js book

    console.log(book.name); // js book
    console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]

寄生式繼承是對原型繼承的二次封裝,並在二次封裝過程中對繼承的對象進行了拓展,這樣新創建的對象不僅僅有父類中的屬性和方法,而且還添加了新的屬性和方法。

5.6 寄生組合式繼承

上面介紹的組合繼承是把類式繼承和構造函數繼承組合使用,這種方式有一個問題,就是子類不是父類的實例,而子類的原型是父類的實例,所以才有了這裏要說的寄生組合繼承。寄生繼承依賴於原型繼承,原型繼承又與類式繼承很像,寄生繼承有些特殊,它處理的不是對象,而是對象的原型。

組合繼承中通過構造函數繼承的屬性和方法是沒有問題的,這裏主要探討通過寄生式繼承重新繼承父類的的原型。我們需要繼承的僅僅是父類的原型,不再需要調用父類的構造函數,也就是在構造函數繼承中我們已經調用了父類的構造函數。因此我們需要的就是父類的原型對象的一個副本,而這個副本我們通過原型繼承可以得到,但是這麽直接賦值給子類會有問題的,因為對父類原型對象復制得到的復制對象p中的constructor指向的不是subClass子類對象,因此在寄生式繼承中要對復制對象p做一次增強處理,修復它的constructor屬性指向不正確的問題,最後得到的復制對象p賦值給子類的原型,這樣子類的原型就繼承了父類的原型並且沒有執行父類的構造函數。測試代碼如下:

    // 寄生式繼承
    function inheritObject(o) {
        // 申明一個過渡函數對象
        function F() {}
        // 過渡對象的原型繼承父對象
        F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
        return new F();
    }
    // 寄生組合繼承
    function inheritPrototype(subClass, superClass) {
        // 復制一份父類的原型副本保存在變量中
        var p = inheritObject(superClass.prototype);
        // 修正因為重寫子類原型而導致子類的constructor屬性被修改
        p.constructor = subClass;
        // 設置子類的原型
        subClass.prototype = p;
    }
    // 定義父類
    function SuperClass(name) {
        this.name = name;
        this.colors = [‘red‘, ‘blue‘, ‘green‘];
    }
    // 定義父類原型方法
    SuperClass.prototype.getName = function () {
        console.log(this.name);
    };
    // 定義子類
    function SubClass(name, time) {
        // 構造函數式繼承
        SuperClass.call(this, name);
        // 子類新增屬性
        this.time = time;
    }
    // 寄生式繼承父類原型
    inheritPrototype(SubClass, SuperClass);
    // 子類新增原型方法
    SubClass.prototype.getTime = function () {
        console.log(this.time);
    }

    var instance1 = new SubClass(‘js book‘, 2014);
    var instance2 = new SubClass(‘css book‘, 2013);
    instance1.colors.push(‘black‘);
    console.log(instance1.colors); //["red", "blue", "green", "black"]
    console.log(instance2.colors); //["red", "blue", "green"]
    instance2.getName(); //css book
    instance2.getTime(); //2013

最大的改變就是對子類原型的處理,被賦予父類原型的一個引用,這是一個對象,因此這裏有一點要註意就是子類再想添加原型方法必須通過prototype對象,通過點語法的方式一個一個添加方法了,否則直接賦予對象就會覆蓋掉從父類原型繼承的對象。

JavaScript原型鏈和繼承