zoukankan      html  css  js  c++  java
  • JavaScript 原型与继承

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

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

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

     

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

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

  • 相关阅读:
    洛谷P3806 【模板】点分治1 【点分治】
    《软件自动化测试开发》出版上市-广而告之
    接口测试用例设计
    接口测试用例设计
    测试的行业危机
    测试的行业危机
    测试的行业危机
    从0开始学正则表达式-基于python
    从0开始学正则表达式-基于python
    从0开始学正则表达式-基于python
  • 原文地址:https://www.cnblogs.com/Yunya-Cnblogs/p/13441218.html
Copyright © 2011-2022 走看看