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的继承处理的和其他语言还是有所不同,构造函数相当于父亲,这个父亲有一个背包就是原型对象。当他的儿子要去用方法时就去找父亲的背包,父亲的背包没找到就找爷爷的背包。

  而在这个背包中有一张字条,就是父亲的名字。

  以上就是原型对象与构造函数的关系。

  使用继承时应当把公共方法丢给背包而不是父亲本身,这是与别的语言比较大的区别。

  除此之外都差不多,更改继承一定要注意是换一个背包,而不是换一个父亲。当然你可以换一个父亲,但是父亲没有带好吃的啊你找不到方法。

posted @ 2020-08-05 17:15  云崖先生  阅读(452)  评论(2编辑  收藏  举报