JavaScript 原型与继承
原型基础
   每个对象都有一个原型prototype对象,通过函数创建的对象也会拥有这个原型对象。
原型是一个指向对象的指针。
原型对象的作用:
存储一些实例对象公用的方法或属性,也就是说一个构造函数中的公共方法或属性应该放入原型对象中
原型对象中的参数:
默认一个原型对象有一个方法
constructor,即构造函数本身。原型对象和构造函数的关系:
构造函数怎么找到自己的原型对象:
使用属性
prototype即可找到该原型对象,你可以为其添加公共方法或属性方便该构造函数的实例对象使用。实例对象怎么找到自己的原型对象:
使用属性
__proto__即可找到该实例对象的原型对象
使用字面量创建出的对象可以调用其原型对象中的方法。
<script>"use strict"; let array = [1, 2, 3]; console.log(array); </script>

构造函数,实例对象,原型对象的关系。

获取原型对象
   如果是一个构造函数,你想获取到原型对象为其实例化的对象添加公共方法,可以使用属性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>

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

更改原型对象
   使用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>

原型检测
   使用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.setPrototypeOf 与Object.getProttoeypOf 替代 __proto__。
   以下示例将展示__proto__原理。

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


以下示例不是在原型对象上继承,故是一种错误的做法。
<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>

方法覆写
由于查找顺序是由下而上,所以我们在最近的原型对象中写入同名方法就不会继续向上查找了。
<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类应该当做工具箱来使用,而不应该作为其他类的父类被继承下来。


<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的继承处理的和其他语言还是有所不同,构造函数相当于父亲,这个父亲有一个背包就是原型对象。当他的儿子要去用方法时就去找父亲的背包,父亲的背包没找到就找爷爷的背包。
而在这个背包中有一张字条,就是父亲的名字。
以上就是原型对象与构造函数的关系。
使用继承时应当把公共方法丢给背包而不是父亲本身,这是与别的语言比较大的区别。

                
            
        
浙公网安备 33010602011771号