JavaScript原型鏈和繼承


1.概念

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

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

2.基於原型鏈的繼承

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

  在ECMA標准中,someObject.[[Prototype]]符號是表示指向someObject的原型。從ES6開始,[[Prototype]]可以通過Object.getPrototypeOf()和Object.setPrototype()訪問器來訪問。除此之外__proto__這個是JavaScript的非標准api,但是很多瀏覽器都實現了__proto__,二者作用等同。注意瀏覽器沒有實現對象的object.Prototype這樣的屬性,即沒有實現對象實例的Prototype屬性,只有構造函數.prototype屬性。

  但是[[Prototype]]和構造函數func的prototype屬性不同,不要弄混。構造函數創建的實例對象的[[prototype]]指向function的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__,指向構造函數的原型對象Foo.prototype,這樣就可以訪問原型對象的所有方法。

4.構造函數Foo()是方法,也是對象,它的__proto__屬性指向它的構造函數的原型對象Function.prototype,這個對象中有共有屬性call(),bind()等。

5.Foo()的原型對象Function.prototype是對象,它的__proto__屬性指向它的構造函數的原型對象,即Object.prototype,這個對象中共有共有屬性length,is()等。

6.function Function()的prototype屬性指向原型對象Function.prototype,該原型對象的constructor屬性指向function Function()本身。

7.Function.prototype的__proto__屬性指向它構造函數的原型對象Object.prototype

8.function object()的__proto__屬性指向構造函數的原型對象Function.prototype,這個對象包含object實例共享的屬性和方法。

9.function ojbect()的prototype屬性指向原型對象Object.prototype

9.最后Object.prototype對象的__proto__指向null

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

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

 

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

5.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)只能把函數的內容輸出,並不能看到函數的原型,函數的原型的原型,只能看到這個方法體,目前本人還沒有搞清楚這個問題。截圖如下:

 

5.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是使用構造方法new Graph()生成的對象,它有自己的屬性‘vertices’和‘edges’,還有從自己的原型對象中繼承的addVertice方法,在g被實例化時,g.[[Prototype]]指向了Graph.prototype

5.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

5.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);

輸出結果如下:

在原型鏈上查找屬性比較耗時,對性能有副作用,試圖訪問不存在的屬性的時候會遍歷整個原型鏈。遍歷對象的屬性時,原型鏈上每個可枚舉屬性都會被枚舉出來。要檢查對象是否有一個自己定義的屬性,而不是從原型鏈上繼承的屬性,可以使用從Object.prototype上繼承的hasOwnPrototype方法。hasOwnPrototype是JavaScript中處理屬性但不會遍歷原型鏈的方法之一,另外可以使用Object.keys()方法。注意這個並不能解決一切問題,沒有這個屬性的時候hasOwnPrototype會返回undefined,可能該屬性存在,但是它的值就是undefined。

經常使用的一個錯誤做法是擴展Object.prototype或其他內置原型,這種技術會破壞封裝,盡管一些流行的框架例如Prototype.js在使用該技術,但是仍然沒有足夠好的理由使用附加的非標准方法來混入內置原型。擴展內置原型唯一的理由是支持JavaScript引擎的新特性,例如Array.forEach,當然在es6中這個特性已經存在。

6. JavaScript中的繼承

6.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關鍵字和方法名來創建新對象時,由於函數外面通過點語法函數名.xxx)添加的屬性和方法沒有執行到,所以新創建的對象中無法使用它們,但是可以通過類名來使用。因此在類外面通過點語法來創建的屬性,方法可以被稱為類的靜態共有屬性和類的靜態共有方法。

參考下面的代碼:

    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和類(Boo()方法)的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關鍵字創建對象b,對Book函數對象內的this指定的屬性賦值,並且將b的原型指向Book.prototype
第二句,輸出b.num,因為num是類的私有屬性,對象訪問不到,在Book.prototype上也找不到,所以輸出undefined
第三句,輸出b.isJSBook,在構造函數內沒有這個屬性,在Book.prototype上有,所以輸出false
第四句,輸出b.id,在構造函數中有這個屬性,它是共有屬性,值為11
第五句,輸出b.isChinese,這個是類的靜態屬性,在類的對象上是找不到的,輸出undefined
第六句,輸出Book.isChinese,這個是類的靜態屬性,使用類名直接訪問,輸出true
第七句,調用Book類的resetTime()方法,這個是類的靜態屬性,輸出new time

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

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

    // 類式繼承
    // 申明父類
    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 instanceof SuperClass); // false
    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

1. 申明父類(函數)SuperClass()
2. 在SuperClass的原型對象上設置共有方法getSuperValue
3. 申明子類(函數)SubClass()
4. 設置子類SubClass的原型對象是父類SuperClass的一個實例,子類繼承了父類的屬性和方法,以及父類的原型對象上的屬性和方法。
5. 在子類SubClass的原型對象設置共有方法getSubValue
6. 定義父類對象sup
7. 定義子類對象sub
8. 調用父類對象sup的getSuperValue()方法得到true
9. 調用父類對象sup的方法getSubValue(),它沒有這個方法,報錯了
10. 調用子類對象sub的getSubValue()方法,在它的內部找不到,在它的原型對象上找,有這個方法,返回this.subValue,返回false
11. 調用子類對象的getSuperValue()方法,在它的內部找不到,原型對象上找不到,繼承的父類的原型對象上有,返回this.superValue,值為true
12. 子類對象sub是子類SubClass的一個實例
13. 子類對象sub是父類SuperClass的一個實例
14. 父類對象sup不是子類SubClass的一個實例
15. 父類對象sup是父類SuperClass的一個實例
16. 子類SubClass不是父類SuperClass的實例
17. 子類的原型對象SubClass.property是父類SuperClass的一個實例
18. 子類的原型對象SubClass.property不是父類原型對象SuperClass.property的實例,因為父類的原型對象不是一個類而是一個對象
19. 子類的原型對象sub.property不是父類SuperClass的一個實例,而是指向一個父類對象

類的原型對象用來為類添加共有方法,但是不能直接添加,訪問這些屬性和方法,必須通過原型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(instance2.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"]

1. 申明父類(函數)SuperClass,內部有共有引用屬性books
2. 申明子類(函數)SubClass,函數內部沒有內容
3. 子類的原型對象設置為父類的一個對象,子類繼承了父類的屬性,方法和父類原型對象上的屬性,方法
4. 定義子類對象instance1instance2,它們繼承了父類的屬性,方法以及父類原型對象上的屬性,方法
5. 輸出子類instance1的屬性books,在子類對象的內部沒有,在在父類上有這個屬性輸出["javascript", "html"]
6. 在子類對象instance2上找books屬性,它來自繼承的父類內部,並且是一個引用屬性,修改這個屬性,添加一個元素“java”
7. 輸出子類對象instance1book屬性,她來自繼承的父類內部,已經被修改,輸出["javascript", "html", "java"]
8. 輸出子類對象instance2book屬性,她來自繼承的父類內部,已經被修改,輸出["javascript", "html", "java"]
9. 在父類函數SuperClass上訪問它內部的屬性books,找不到這個屬性,輸出undefined
10. 定義父類對象sup1,和sup2,他們調用父類函數,初始化共有屬性books,通過new命令創建,是兩個完全不同的對象
11. 給父類對象sup2的引用屬性books添加一個元素“css”
12. 輸出父類對象sup1的屬性books,輸出["javascript", "html"],這個books屬性和sup2的books屬性是沒有關系的
13. 輸出父類對象sup2的屬性books,輸出["javascript", "html", "css"]

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

其二,由於子類實現繼承是靠其原型prototype對父類的實例化實現的,因此在(實例化子類時會創建父類,就是這一句:let sub = new SubClass();)創建父類的時候是無法向父類傳遞參數的,因此在實例化父類的時候無法調用父類的構造函數進而對父類構造函數內部的屬性初始化。 

6.3 構造函數繼承—call方法創建繼承

    // 構造函數繼承
    // 申明父類
    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"]

1. 申明父類方法SuperClass,方法內部有共有屬性books,id
2. 給父類的原型對象上申明共有方法showBooks()
3. 申明子類方法SubClass(),在子類方法中使用call調用父類SuperClass方法,在當前子類中執行父類方法,給this賦值,這樣子類就繼承了父類內部的方法和屬性(id,books),但是子類不會繼承父類的原型對象中的屬性和方法(showBooks())
4. 申明兩個子類實例instance1instance2,並分別傳參給父類10,11
5. 修改子類實例instance1books屬性,這是一個引用屬性,給數組添加一個元素“java”
6. 輸出子類實例instance1books屬性,“java”已經被添加上去了
7. 輸出子類實例instance1的id屬性是10
8. 輸出子類實例instance2books屬性,這里是沒有“java”元素的,因為它是在調用call方法的時候直接復制的一份,和instance1的是兩個完全不同的數組對象
9. 輸出子類實例instance2books屬性是11
10. 調用子類實例instance1showBooks()方法,在子類中找不到,子類的原型對象中找不到,在子類繼承的父類中找不到(這里不會在子類繼承的父類的原型對象中找這個方法),因此報錯
11. 調用子類實例instance2showBooks()方法,在子類中找不到,子類的原型對象中找不到,在子類繼承的父類中找不到(這里不會在子類繼承的父類的原型對象中找這個方法)因此報錯
12. 申明父類實例instance3,傳入參數12
13. 調用父類實例instance3的showBooks()方法,在父類內部找不到這個方法,在父類的原型對象中有這個方法,輸出books對象,注意這個對象並沒有被子類實例instance1修改,所有子類實例都有一份自己單獨的屬性和方法

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

6.4 組合繼承

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

    // 申明父類
    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

1. 申明父類方法SuperClass(),方法內部有共有屬性
2. 在父類方法的原型對象上定義共有方法getname(),輸出當前屬性name
3. 申明子類方法SubClass(),在子類方法中使用call調用父類方法,在當前子類中執行父類方法給this賦值,這樣子類就繼承了父類內部的方法和屬性(name,books),但是子類不會繼承父類的原型對象中的屬性和方法。子類方法中有共有屬性time
4. 子類的原型對象設置為父類的一個實例對象,子類繼承了父類的屬性,方法和父類原型對象上的屬性,方法。
5. 在子類的原型對象上定義共有方法getTime(),輸出當前對象的屬性time
6. 定義子類對象實例instance1,分別向父類構造函數傳參“js book”,子類構造函數傳遞參數2014
7. 訪問子類對象實例instance1的books屬性,在子類方法中找不到books屬性,在子類對象實例的原型對象的構造函數內有這個屬性,給這個引用屬性添加一個元素“java”
8. 訪問子類對象實例instance1的getName()方法,在子類方法構造函數中找不到,在子類原型對象中有這個方法,輸出“js book”
9. 訪問子類對象實例instance1的getTime()方法,在子類方法構造函數中找不到,在子類原型對象中有這個方法,輸出2014
10. 定義子類對象實例instance2,分別向父類構造函數“css book”,子類構造函數傳遞參數2013
11. 訪問子類對象實例instance2books屬性,在子類方法中找不到books屬性,這里是構造函數繼承,在子類對象實例的原型對象的構造函數內有這個屬性,這個屬性是從父類構造函數中拷貝的一份,它和instance1的books屬性是不同的,相互沒有影響
12. 訪問子類對象實例instance2的getName()方法,子類構造函數中找不到,父類構造函數中找不到,父類原型對象上有這個方法,輸出當前對象的name屬性,因此輸出“css book”
13. 訪問子類對象實例instance2的getTime()方法,子類構造函數中找不到,子類原型對象中有這個方法,輸出當前對象的time屬性,因此輸出2013

注意這里通過call方式繼承父類后,訪問方法的先后順序是:
1. 子類方法中的共有方法SubClass.this.getName,
2. 父類方法中的共有方法SuperClasss.this.getName,
3. 子類原型對象中的共有方法SubClass.prototype.getName,
4. 父類原型對象中的共有方法SuperClasss.prototype.getName

訪問屬性books的時候也是這個順序,所以優先考慮通過call方法給當前this賦值得到的books,而不是通過原型對象繼承的books。

在子類構造函數中執行父類構造函數,在子類原型上實例化父類就是組合模式。通過this將引用屬性books定義在父類的共有屬性中,每次實例化子類都會單獨拷貝一份,因此在子類的實例中更改父類繼承下來的引用類型屬性books不會影響到其他實例,並且子類實例化過程中又能將參數傳遞到父類的構造函數中。

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

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

原型式繼承的思想是借助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"]

1. 定義原型式繼承方法,在方法內部申明過渡類,設置類的原型對象為傳入的參數,訪問這個對象實例,這個實例繼承了父類對象
2. 定義book對象,對象內有name屬性和alikeBook屬性
3. 定義子類對象newBook,調用原型式繼承方法,繼承book對象中的屬性
4. 訪問子類對象newBookname屬性,賦值為“ajax book”,子類對象的原型對象中有這個屬性,並且是一個值類屬性
5. 訪問子類對象newBookalikeBook屬性,添加元素“xml book”,子類對象的原型對象中有這個屬性,並且是一個引用屬性
6. 定義子類對象otherBook,調用原型式繼承方法,繼承book對象中的屬性
7. 訪問子類對象otherBookname屬性,賦值為“ajax book”,子類對象的原型對象中有這個屬性,並且是一個引用類型變量
8. 訪問子類對象otherBookalikeBook屬性,添加元素“as book”,子類對象的原型對象中有這個屬性,並且是一個引用類型變量
9. 輸出newBookname屬性,值是“ajax book”
10. 輸出newBookbooks屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組內被添加了 “as book”,輸出['css book', 'html book', 'xml book', 'as book']
11. 輸出othername屬性,值是“flash book”
12. 輸出oterBookbooks屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組內被添加了 “as book”,輸出['css book', 'html book', 'xml book', 'as book']
13. 輸出父類對象的name屬性,值是“js book”
14. 輸出父類對象book的的alikeBook屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組被修改過了,添加了“as book”,輸出['css book', 'html book', 'xml book', 'as book']

和類式繼承一樣,父類對象book中的值類型被復制,引用類型屬性被共用,它也有類式繼承的缺點,即修改修改子類中從父類繼承來的引用類型屬性,會影響到其他子類中的同名屬性,他們是同一個屬性。這種方法的優點是F()函數內部沒有什么內容,開銷比較小,還可以將F過渡類緩存起來。也可以使用新的語法Object.create()來代替這一句。不過創建子類實例的時候是可以向父類構造函數傳參的,這里不再展開介紹。

6.6 寄生式繼承—增強版的原型式繼承

    // 原型式繼承
    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"]

1. 聲明原型式繼承方法inheritObject,實現原型式繼承
2. 定義父類對象book
3. 聲明創建Book對象的方法createBook,方法內部使用new表達式創建一個繼承自傳遞參數的對象o,在這個對象上擴展屬性,最后返回創建的對象
4. 使用createBook方法創建對象newBook,傳遞參數是book對象
5. 訪問對象newBookname屬性,在它的原型對象上有這個屬性,重新賦值為“ajax book”
6. 訪問對象newBookalikeBook屬性,在它的原型對象上有這個屬性,添加元素“xml book”,這樣會影響所有繼承自這個對象的對象
7. 使用createBook方法創建對象otherBook,傳遞參數是book對象
8. 訪問對象otherBookname屬性,在它的原型對象上有這個屬性,重新賦值為“flash book”
9. 訪問對象otherBookalikeBook屬性,在它的原型對象上有這個屬性,添加元素“as book”,這樣會影響所有繼承自這個對象的對象
10. 訪問newBook的屬性name,雖然繼承自它的原型對象的,但是這個屬性是值類型,已經被修改成“ajax book”
11. 訪問newBookgetName方法,這個方法是通過在原型對象上擴展的方法繼承的,輸出傳入參數的name屬性,值為“js book”
12. 訪問newBookalikeBook屬性,這個屬性是繼承自原型對象的,並且是一個引用類型,已經被修改成["css book", "html book", "xml book", "as book"]
13. 訪問otherBook的屬性name,雖然繼承自它的原型對象的,但是這個屬性是值類型,已經被修改成“flash book”
14. 訪問otherBookalikeBook屬性,這個屬性是繼承自原型對象的,並且是一個引用類型,已經被修改成["css book", "html book", "xml book", "as book"]
15. 訪問otherBookgetName方法,這個方法是通過在原型對象上擴展的方法繼承的,輸出傳入參數的name屬性,值為“js book”
16. 訪問父對象bookname屬性,它仍然是“js book”
17. 訪問父類對象bookalikeBook屬性,這個屬性已經被通過原型對象繼承book對象的子類對象修改了,已經被修改成["css book", "html book", "xml book", "as book"]

寄生式繼承是對原型繼承的二次封裝,並在二次封裝過程中對繼承的對象進行了拓展,這樣新創建的對象不僅僅繼承了父類中的屬性和方法,而且還添加了新的屬性和方法。之所以叫寄生式繼承,是指可以像寄生蟲一樣寄托於某個對象的內部生長,寄生式繼承這種增強新創建對象的繼承方式是依托於原型繼承模式。

從上面的測試代碼可以看出,這種方式仍然會有所有子類共用一個引用實例的問題,只要涉及原型繼承都會有公用對象屬性修改問題。

6.7 寄生組合式繼承-改造組合繼承

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

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

    /**
     * 原型式繼承
     * @param o 父類
     * */
    function inheritObject(o) {
        // 申明一個過渡函數對象
        function F() {}
        // 過渡對象的原型繼承父對象
        F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
        return new F();
    }

    /**
     * 寄生式繼承,繼承原型
     * @param subClass 子類
     * @param superClass 父類
     */
    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
    console.log(SubClass instanceof SuperClass); // false
    console.log(SubClass.prototype instanceof SuperClass); // true
    console.log(SubClass.prototype instanceof SuperClass.prototype); // Right-hand side of 'instanceof' is not callable
    console.log(instance2 instanceof SubClass); // true
    console.log(instance2 instanceof SuperClass); // true

1. 定義原型式繼承方法inheritObject,通過過渡對象返回一個通過原型對象繼承自傳入自參數的實例
2. 定義寄生式繼承方法inheritPrototype,傳入父類和子類。復制一份父類原型的副本保存在變量中,修正因為重寫子類原型而導致子類的constructor屬性問題,設置子類的原型為這個對象。
3. 定義父類方法SuperClass,內部有自己的屬性
4. 訪問父類的原型對象,添加getName方法,輸出當前屬性name
5. 定義子類方法SubClass,在子類中調用call方法,在子類中執行父類的構造方法,給子類的this賦值。定義子類自己的屬性time
6. 調用寄生式繼承方法inheritPrototype,先拷貝父類原型對象賦值給變量p,修改它的constructor屬性,讓它指向子類構造函數subClass,設置子類的原型對象為這個新的對象p
7. 訪問子類SubClass的原型對象,設置共有方法getTime,輸出當前對象time
8. 定義子類對象實例instance1,傳入兩個參數,第一個“js book”傳遞給父類方法,第二個2014用於對象自己的共有屬性
9. 定義子類對象實例instance2,傳入兩個參數,第一個“css book”傳遞給父類方法,第二個2013用於對象自己的共有屬性
10. 訪問對象instance1colors屬性,它是通過call方法從父類構造函數中單獨拷貝的,給colors屬性新加一個元素“black”
11. 訪問對象instance1colors屬性,輸出的是新增“black”元素之后的數組
12. 訪問對象instance2colors屬性,它是通過call方法從父類構造函數中單獨拷貝的,這個沒有被修改過
13. 訪問對象instance2getName方法,它是通過父類的原型對象繼承來的,輸出當前對象的name屬性“css book”
14. 訪問對象instance2getTime方法,它是通過子類對象的原型對象繼承來的,輸出當前對象的time屬性2013
15. SubClass子類不是父類SuperClass的實例
16. 子類原型對象SubClass.prototype是父類SuperClass的實例
17. 子類原型對象SubClass.prototype不是父類原型對象SuperClass.prototype的實例
18.子類對象instance2是子類SubClass的實例
19. 子類對象instance2是父類SuperClass的實例

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

從上面的例子來看,寄生組合繼承還解決了子類共用父類中引用類型屬性的問題,子類中繼承的引用類型實例互不影響。還有子類也繼承了父類原型中的屬性和方法

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM