JavaScript 原型與繼承


原型基礎

  每個對象都有一個原型prototype對象,通過函數創建的對象也會擁有這個原型對象。

  原型是一個指向對象的指針。

  原型對象的作用:

    存儲一些實例對象公用的方法或屬性,也就是說一個構造函數中的公共方法或屬性應該放入原型對象中

  原型對象中的參數:

    默認一個原型對象有一個方法constructor,即構造函數本身。

  原型對象和構造函數的關系:

    構造函數是造一個對象,原型對象即是為構造函數創建出的實例對象提供公共的屬性和方法

  構造函數怎么找到自己的原型對象:

    使用屬性prototype即可找到該原型對象,你可以為其添加公共方法或屬性方便該構造函數的實例對象使用。

  實例對象怎么找到自己的原型對象:

    使用屬性__proto__即可找到該實例對象的原型對象

  使用字面量創建出的對象可以調用其原型對象中的方法。

<script>"use strict";

    let array = [1, 2, 3];

    console.log(array);

</script>

image-20200804182414085

  構造函數,實例對象,原型對象的關系。

image-20200804231308424

獲取原型對象

  如果是一個構造函數,你想獲取到原型對象為其實例化的對象添加公共方法,可以使用屬性prototype來獲取。

  如果是一個已經實例化好的對象,你想獲取到其原型對象可以使用屬性__proto__來進行獲取,也可以使用Object.getPrototypeOf()方法來進行獲取。

<script>"use strict";

    function User() {  };  // 構造函數

    console.log(User.prototype); 

    let u1 = new User();

    console.log(u1.__proto__);
    console.log(Object.getPrototypeOf(u1));


    console.log(u1.__proto__ === User.prototype);  // true
    console.log(u1.__proto__ === Object.getPrototypeOf(u1));  // true
    console.log(User.prototype === Object.getPrototypeOf(u1));  // true
</script>

原型對象設置方法

  函數擁有多個原型,prototype 用於實例對象使用,__proto__用於函數自身當做對象時使用。

  注意函數本身也是一個實例對象,所以當將函數作為對象使用時使用__proto__為它設置方法。

  當函數作為構造函數時其供實例使用的方法應該存儲在prototype中,這是為了大幅度節省內存。

  否則每一個實例對象都會創建出自己的方法。

<script>"use strict";

    function User() {  };  // 構造函數

    User.__proto__.show = function (){
        console.log("函數作為對象調用的方法...");
    };

    User.show();  // 函數作為對象調用的方法...
// =============

    User.prototype.show = function(){
        console.log("該函數的實例對象調用的方法...");
    };

    let u1 = new User();

    u1.show();  // 該函數的實例對象調用的方法...
    
</script>

  推薦使用prototype來設置方法,因為將函數作為對象來使用的場景不多見。

  設置方式有兩種,第一種在原有的原型對象基礎上增加新的方法,第二種是覆蓋原本的原型對象,但是要注意添加參數constructor來指向構造函數。

<script>"use strict";

    function User() {  };  // 構造函數
// =========  在原有的原型對象基礎上新增一個方法

    User.prototype.show = function(){
        console.log("該函數的實例對象調用的方法...");
    };

    let u1 = new User();

    u1.show();  // 該函數的實例對象調用的方法...
    
</script>
原型對象中單獨新增方法
<script>"use strict";

    function User() { };  // 構造函數
// =========  // 設置新的原型對象

    User.prototype = {

        constructor: User, // 必須添加該參數,指向構造函數。

        show() {
            console.log("方法1");
        },

        test() {
            console.log("方法2");
        }

    };

    let u1 = new User();

    u1.show();  // 方法1
    u1.test();  // 方法2
</script>
設置新的原型對象

原型鏈關系圖

  原型對象也有自己的原型,最終的原型對象都是Object.prototype

<script>"use strict";

    function User() { };  // 構造函數

    User.prototype.show = function () { console.log("該函數的實例對象調用的方法..."); };

    let u1 = new User();

    console.log("User的實例對象的原型對象--->", u1.__proto__);

</script>

image-20200804233906229

<script>"use strict";

    function User() { };  // 構造函數

    User.prototype.show = function () { console.log("該函數的實例對象調用的方法..."); };

    let u1 = new User();

    // 驗證上圖關系
    console.log(u1.__proto__.__proto__ === Object.prototype);  // true
</script>

原型對象與構造函數

  原型對象中有一個constructor的方法,即指向構造函數。

<script>"use strict";

    function User() { };  // 構造函數

    console.log(User.prototype.constructor  === User);  // true
</script>

image-20200805000155803

更改原型對象

  使用Object.setPrototypeOf() 可設置對象的原型對象。

  也可使用Object.create()來設置對象的原型對象,這個方法下面會介紹到。

<script>

    "use strict";

    function User() {

    };  // 構造函數

    function Admin() { }; // 構造函數


    User.prototype.show = function () {
        console.log("User中的show");
    };


    Admin.prototype.show = function () {
        console.log("Admin中的show");
    };


    let a1 = new Admin();

    Object.setPrototypeOf(a1, User.prototype);  // 將a1的原型對象設置為User的原型對象

    console.log(Object.getPrototypeOf(a1));  // {show: ƒ, constructor: ƒ}

    a1.show(); // User中的show

    let a2 = new Admin();
    a2.show(); // Admin中的show

</script>

image-20200805002809868

原型檢測

  使用instanceof檢測構造函數的 prototype 屬性是否出現在某個實例對象的原型鏈上

  使用isPrototypeOf檢測一個對象是否是另一個對象的原型鏈中

<script>

    "use strict";

    function User() {

    };  // 構造函數


    let u1 = new User();

    console.log(u1 instanceof User);  // true  u1的原型鏈中包含User的原型對象嗎?

    console.log(User.prototype.isPrototypeOf(u1)); // true User的原型對象在u1的原型鏈中嗎?

</script>

屬性遍歷

  使用in 檢測原型鏈上是否存在屬性,使用 hasOwnProperty() 只檢測當前對象的原型對象。

  使用for/in會按照原型鏈遍歷。

<script>

    "use strict";

    function User() {

    };  // 構造函數

    User.prototype = {

        constructor: User,

        show() {
            console.log("User原型的show...");
        }
    }


    let u1 = new User();

    console.log("show" in u1);  // true  會沿着原型鏈查找

    console.log(u1.hasOwnProperty("show")); // false  只檢測自己

    for (let key in u1) {    // for/in會遍歷所有原型鏈
        if (key === "show") {
            console.log("存在");  // 存在
        }
    }

</script>

原型借用

  我們可以借用另一個原型對象中的方法,使用call()或者apply()來改變this指向與傳遞參數即可。

  如下示例,對象借用了數組中的排序方法對成績進行排序,這里實在想不到太好的例子。所以就用這個了。

<script>

    "use strict";

    let obj = {
        Html: 76,
        Css: 88,
        Js: 100,
        Python: 96,
        Linux: 77,
    };
    // call傳遞一個新的this指向
    let res = Array.prototype.sort.call(Object.entries(obj), function (v1, v2) {
        return v2[1] - v1[1];
    });

    obj = {};  // 清空對象

    for (let i = 0; i < res.length; i++) {

        let [key, value] =  res[i];
       
        Object.assign(obj,{[key]:value})

    };

    console.log(obj);  // {Js: 100, Python: 96, Css: 88, Linux: 77, Html: 76}

</script>

this

  this 不受原型繼承影響,this 指向調用屬性時使用的對象。

<script>

    "use strict";

    function User(username) {

        this.username = username;

    };  // 構造函數

    User.prototype = {

        constructor: User,

        show() {
            console.log(this.username);
        }
    }


    let u1 = new User("u1");

    let u2 = new User("u2");

    u1.show();  // u1
    u2.show();  // u2

</script>

Object.create

  該方法可以立即返回一個對象,參數1指定其原型對象,參數2可設置其屬性或方法及其特征。

<script>

    "use strict";

    // 無原型的對象

    let obj_1 = Object.create(null, {
        username: {
            value: "雲崖"
        }
    });

    console.log(obj_1);  // username: "雲崖"
    

    // 有原型的對象,該對象原型指向為Array對象的原型

    let obj_2 = Object.create(Array.prototype, {
        username: {
            value: "雲崖"
        }
    });

    console.log(obj_2); // Array {username: "雲崖"}
  
</script>

__proto__原理

  __proto__其實它並非一個真正意義上的屬性而是使用getattr以及setattr進行實現的。

  建議使用 Object.setPrototypeOfObject.getProttoeypOf 替代 __proto__

  以下示例將展示__proto__原理。

image-20200805143542854

<script>

    "use strict";

    function User(username) {

        this.username = username;

        Object.defineProperties(this, {

            __proto__: {

                get() {
                    return User.prototype;
                },
                set(value) {
                    Object.setPrototypeOf(this, value);
                },
            },

        });
    };  // 構造函數

</script>

繼承與多態

  Js的繼承是原型上的繼承。Js只有單繼承,沒有多繼承,即一個對象只能有一個原型。

  當一個對象開始找方法時不斷的向上使用__proto__來尋找方法。

  調用相同方法,產生不同結果,這就是多態的體現。

繼承實現

  注意!Js的繼承是原型對象的繼承,並不是類的繼承。

  當一個實例對象要找方法時會一層一層向上找,如果找到了方法就不再繼續向上找了。

<script>

    "use strict";

    function A() { }; // 構造函數

    A.prototype.f1 = function () {
        console.log("A的f1方法");
    };

    function B() { }; // 構造函數

    Object.setPrototypeOf(B.prototype, A.prototype);  // B的原型對象繼承於A的原型對象

    B.prototype.f2 = function () {
        console.log("B的f2方法");
    };


    function C() { }; // 構造函數  

    Object.setPrototypeOf(C.prototype, B.prototype);  // C的原型對象繼承於B的原型對象

    C.prototype.f3 = function () {
        console.log("C的f3方法");
    }

    let c1 = new C();

    console.dir(c1);

    c1.f1();
    c1.f2();
    c1.f3();

</script>
正確的原型對象繼承

image-20200805150745647

image-20200805170023900

  以下示例不是在原型對象上繼承,故是一種錯誤的做法。

<script>

    "use strict";

    function A() { };

    A.prototype.f1 = function () {
        console.log("A的f1方法");
    };

    function B() { }; 

    Object.setPrototypeOf(B, A.prototype);  

    B.prototype.f2 = function () {
        console.log("B的f2方法");
    };


    function C() { }; 

    Object.setPrototypeOf(C, B.prototype); 

    C.prototype.f3 = function () {
        console.log("C的f3方法");
    }

    let c1 = new C();

    console.dir(c1);

    // 異常
    c1.f1(); 
    c1.f2();  
    c1.f3();

</script>
錯誤的構造函數繼承

image-20200805170233806

方法覆寫

  由於查找順序是由下而上,所以我們在最近的原型對象中寫入同名方法就不會繼續向上查找了。

<script>

    "use strict";

    function A() { }; // 構造函數

    A.prototype.show = function () {
        console.log("A的show方法");
    };

    function B() { }; // 構造函數

    Object.setPrototypeOf(B.prototype, A.prototype);  // B的原型對象繼承於A的原型對象

    B.prototype.show = function () {
        console.log("B的show方法");
    };

    let b1 = new B();

    b1.show();  // B的show方法

</script>

多態體現

  同樣的方法運用在不同的對象身上會產生不同的結果,這就是多態的體現。

<script>

    "use strict";

    function User() { }
    User.prototype.show = function () {
        console.log(this.description());  // 調用相同方法,產生不同結果,這就是多態的體現
    };

    function Admin() { }
    Admin.prototype = Object.create(User.prototype);  // Object.create() 也是可以改變對象的原型
    Admin.prototype.description = function () {
        return "管理員在此";
    };

    function Member() { }
    Member.prototype = Object.create(User.prototype);
    Member.prototype.description = function () {
        return "我是會員";
    };

    function Enterprise() { }
    Enterprise.prototype = Object.create(User.prototype);
    Enterprise.prototype.description = function () {
        return "企業帳戶";
    };

    for (const obj of [new Admin(), new Member(), new Enterprise()]) {
        obj.show();
    }


</script>

深究繼承

  繼承是為了復用代碼,繼承的本質是將原型指向到另一個對象。

構造函數

  如果多個構造函數在功能上極其相似,我們希望進行復用代碼則可以利用其它構造函數來進行函數的構建。但是要注意如下問題:

  此時 this 指向了window,無法為當前對象聲明屬性。

<script>

    "use strict";

    function User(username) {

        this.username = username;  // 嚴格模式拋出異常!此時的this指向在window

    }

    User.prototype = {

        constructor: User,

        show() {

            console.log(`this指向-->${this}`);
            console.log(this.username);
        },
    }

    function Admin(username) {
        User(username)
    }

    Object.setPrototypeOf(Admin.prototype, User.prototype);

    let a1 = new Admin("雲崖");
    a1.show();

</script>

  解決上面的問題是使用 call()/apply() 方法改變this指向,從而為每個生成的對象設置屬性。

<script>

    "use strict";

    function User(username) {

        this.username = username;

    }

    User.prototype = {

        constructor: User,

        show() {

            console.log(`this指向-->${this}`);  // this指向-->[object Object]
            console.log(this.username);  // 雲崖
        },
    }

    function Admin(username) {
        User.call(this, username)  // 解決辦法
    }

    Object.setPrototypeOf(Admin.prototype, User.prototype);

    let a1 = new Admin("雲崖");
    a1.show();

</script>

原型工廠

  原型工廠是將繼承的過程封裝,使用繼承業務簡單化。

<script>

    "use strict";

    function extend(sub, sup) {

        // 原型工廠代碼封裝

        Object.setPrototypeOf(sub.prototype,sup.prototype);  // 使sub的原型對象繼承於sup的原型對象
    }



    function User(username) {

        this.username = username;

    }

    User.prototype = {

        constructor: User,

        show() {

            console.log(`this指向-->${this}`);  // this指向-->[object Object]
            console.log(this.username);  // 雲崖
        },
    }

    function Admin(username) {
        User.call(this, username) 
    }

    extend(Admin,User);   // 使用原型工廠封裝

    let a1 = new Admin("雲崖");
    a1.show();

</script>
原型工廠

對象工廠

  在原型繼承基礎上,將對象的生成使用函數完成,並在函數內部為對象添加屬性或方法。

<script>

    "use strict";

    function User(name, age) {
        this.name = name;
        this.age = age;
    }

    User.prototype.show = function () {

        console.log(this.name, this.age);

    };


    function Admin(name, age) {

        let instance = Object.create(User.prototype); // 創建了一個新對象

        User.call(instance, name, age);

        instance.role = function () {
            console.log('admin.role');
        }
        return instance;

    }

    let hd = Admin("管理員", 19);
    hd.show();

</script>
對象工廠

Mixin機制

  由於Js不支持多繼承,所以想添加功能必須在某一個原型對象上不斷的增加功能,這勢必會讓其本來的原型顯得混亂不堪。

  這種時候就可以使用Mixin機制來實現。

  注意:Minin類應該當做工具箱來使用,而不應該作為其他類的父類被繼承下來。

image-20200805160602907

<script>

    "use strict";

    function extend(sub, sup) {

        // 更改原型對象的函數

        Object.setPrototypeOf(sub.prototype, sup.prototype);  // 使sub的原型對象繼承於sup的原型對象

    }


    function Vehicle(name) {
        // 交通工具
        this.name = name;
    }

    Vehicle.prototype = {

        constructor: Vehicle,

        whistle() {
            console.log(`${this.name}在鳴笛`);  // 公用方法放父類中
        },
    }

    function Aircraft(name) {
        // 飛機
        Vehicle.call(this, name);
    }
    extend(Aircraft, Vehicle)  // 飛機的原型對象繼承於交通工具。因此飛機具有了鳴笛方法


    function Car(name) {
        // 汽車
        Vehicle.call(this, name);

    }
    extend(Car, Vehicle)  // 汽車的原型對象繼承於交通工具。因此汽車具有了鳴笛方法


    let Flyable_Mixin = {
        // 飛行器的功能
        fly() {
            console.log(`${this.name}在飛`);
        },
        outer() {
            console.log("其他功能...");
        },
    };


    Object.assign(Aircraft.prototype, Flyable_Mixin); //給飛機添加上飛機Mixin的功能

    let Car_Mixin = {
        // 汽車的功能

        reversing() {
            console.log(`${this.name}正在倒車入庫`);
        },
        outer() {
            console.log("其他功能...");
        },
    };

    Object.assign(Car.prototype, Car_Mixin); //給汽車添加汽車Mixin的功能


    let c1 = new Car("法拉利");
    let a1 = new Aircraft("波音747");


    c1.whistle();  //  法拉利在鳴笛
    c1.reversing();  // 法拉利正在倒車入庫

    a1.whistle();  // 波音747在鳴笛
    a1.fly();  // 波音747在飛

</script>
代碼實現

super

  super會在其原型對象上找。

<script>

    "use strict";

    let a = {
        username: "雲崖"
    };

    let b = {
        __proto__: a,

        show() {
            console.log(super.username);  // super會去找__proto__,相當於拿到a.username
        },
    };

    b.show(); // 雲崖

</script>

總結

  其實Js的繼承處理的和其他語言還是有所不同,構造函數相當於父親,這個父親有一個背包就是原型對象。當他的兒子要去用方法時就去找父親的背包,父親的背包沒找到就找爺爺的背包。

  而在這個背包中有一張字條,就是父親的名字。

  以上就是原型對象與構造函數的關系。

  使用繼承時應當把公共方法丟給背包而不是父親本身,這是與別的語言比較大的區別。

  除此之外都差不多,更改繼承一定要注意是換一個背包,而不是換一個父親。當然你可以換一個父親,但是父親沒有帶好吃的啊你找不到方法。


免責聲明!

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



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