07-继承

作者: LeoCong | 来源:发表于2017-05-22 15:24 被阅读0次

    继承

    Javascript中继承都基于两种方式:
    1.通过原型链继承,通过修改子类原型的指向,使得子类实例通过原型链查找,实现继承父类的属性和方法。这种方式可以让所有实例共享原型链上的方法和属性。但当某个实例修改引用类型的属性时,其他实例也会受影响(引用地址相同)。
    2.借用构造函数,通过apply、call方法在子类中调用父类构造函数,子类实例继承了父类构造函数的实例属性和方法。这个方式继承的属性和方法相互独立,但实例方法重复创建,没有共享。

    • 原型链继承
      原理:将父类的实例对象作为子类的原型对象。
      原型链继承的实质是重写子类的原型对象,将父类的实例作为子类的新原型,于是子类新原型就继承了父类所有的属性和方法。
        function Parent() {}; // 父类
        function Child() {}; // 子类
        var c1 = new Child(); // 继承前子类的实例
        Child.prototype = new Parent();//将子类的原型重写为父类的实例
        var p1 = new Child(); // 继承后子类的实例
        // 没有修改Child.constructor时
        console.log(c1.__proto__.constructor.prototype === p1.__proto__); // true
        console.log(p1.__proto__.__proto__.constructor === Parent); // true
    
    原型链继承.png

    将父类的实例对象作为子类的原型对象,也就是将原来存在父类实例中的属性和方法,继承给了子类的原型对象。
    当读取某个子类实例对象的属性时:1.首先在子类实例中查找;2.如果没找到,就到子类的原型(也就是父类的实例)中查找;3.如果没找到,就在父类的原型中查找;4.如果没找到,就到Object.prototype中查找。

        function Parent () {
            // 父类的实例属性
            this.name = 'Parent'; //基本类型的实例属性,实例之间相互独立
            this.age = [30, 35];// 引用类型的实例属性,所有实例共享
        }
        // 父类的原型方法
        Parent.prototype.getName = function () {
            console.log(this.name);
        }
        function Child () {}
        // 将父类的实例对象作为子类的原型对象
        Child.prototype = new Parent();
        // 子类原型的继承了父类实例的所有属性和方法
        var p1 = new Child();
        var p2 = new Child();
        p1.getName(); // Parent, 子类继承了父类的原型方法
        console.log(p1.age); // [30, 35],子类继承了父类的实例属性
        // 修改子类实例的属性
        p1.name = 'Tom'; // 修改基本类型的属性
        p1.age.push(40); // 修改引用类型的属性
        // 基本类型属性值相互独立
        p1.getName(); // Tom
        p2.getName(); // Parent
        // 引用类型的属性被所有实例共享,修改某一实例的值会影响其他实例
        console.log(p1.age); // [30, 35, 40]
        console.log(p2.age); // [30, 35, 40]
    

    子类覆盖父类中某个方法或添加父类中不存在的某个方法时,需要写在替换原型语句的后面。
    并且不能使用字面量(相当于Object的实例)的方法创建子类原型方法,否则会覆盖之前的原型。
    原型链继承的缺点:
    1.使用原型链继承时,父类的实例成为子类的原型,父类的实例属性就变成了原型属性。而引用类型值的原型属性会被所有实例共享。
    2.创建子类实例时,不能向父类构造函数传递参数。

    • 借用构造函数继承
      原理:在子类构造函数中,调用父类构造函数。(没有用到原型)
      使用call()或apply()方法可以在创建子类实例时,调用执行父类构造函数,这样子类实例就会执行父类构造函数定义的初始化代码。
      创建子类实例时,可以向父类传递参数。并且子类实例不会共享父类引用类型属性,因为继承的都是父类的实例属性和方法,不在原型链上。
        function Parent(name) {
            this.name = name;
            this.age = [30, 35];
        }
        function Child(name) {
            // 调用父类的构造函数
            // Parent.call(this, name);
            Parent.apply(this, arguments);
        }
        var p1 = new Child('Leo');
        var p2 = new Child('Tom');
        console.log(p1.name); // Leo
        console.log(p1.age); // [30, 35]
        p1.age.push(40);
        console.log(p1.age); // [30, 35, 40]
        console.log(p2.age); // [30, 35]不会共享父类引用类型属性
    

    借用构造函数继承的缺点:
    1.只能继承父类的实例属性和方法,不能继承原型属性和方法(没有使用原型)。
    2.方法都在定义在构造函数中,每次创建实例都会创建一遍方法,无法实现函数复用,影响性能。

    • 组合继承
      原理:通过原型链实现原型属性和方法的继承,通过借用构造函数来实现实例属性和方法的继承。
      一般在构造函数上定义实例属性,在原型上定义方法,通过组合继承既满足方法的复用,又保证实例有自己的属性。
        function Parent(name) {
            this.name = name;
            this.colors = ['blue', 'red'];
        }
        Parent.prototype.getName = function () {
            console.log(this.name);
        }
        function Child(name, age) {
            // 调用父类的构造函数
            Parent.call(this, name); // 第二调用Parent()
            this.age = age;
        }
        // 原型链继承
        Child.prototype = new Parent();// 第一调用Parent()
        Child.prototype.constructor = Child;
        var p1 = new Child('Leo', 26);
        var p2 = new Child('Tom', 20)
        p1.colors.push('green');
        p1.getName(); // Leo
        console.log(p1.age); // 26
        console.log(p1.colors); // ["blue", "red", "green"]
        console.log(p2.colors); // ["blue", "red"]
    

    组合继承的缺点:
    调用两次父类构造函数。第一次是在重写子类原型的时,将子类的原型指向父类实例,此时子类原型会继承父类所有的属性和方法(包括实例属性和方法及原型属性和方法)。第二次是在子类调用构造函数创建实例时,在子类构造函数中调用父类构造函数,此时新创建的子类实例对象的实例属性和方法(直接继承来自父类构造函数的实例属性和方法)会覆盖子类原型中的同名实例属性和方法。

    • 原型式继承
      原理:使用Object.create()方法,将传入的对象作为空构造函数的原型,并返回这个空构造函数的实例。
      这种方法不需要创建构造函数,只需传入一个对象,就能从传入的对象衍生出新的对象。本质上还是通过原型链,实例的原型指向传入的对象。
      Object.create()方法接受一个对象为参数,必须传一个对象否则报错,返回一个新的对象,传入的对象作为新对象的原型。
      该方法还可接受第二个参数,该参数是属性描述对象,它所描述的对象属性,会添加到新对象。
      Object.create()方法生成的对象,继承了它原型对象的构造函数。
        // 模拟Object.create()
        function create(obj) {
            function F() {}
            F.prototype = obj;
            return new F();
        }
    

    使用Object.create()实现继承。

        function Parent(name) {
            this.name = name;
            this.colors = ['blue', 'red'];
            this.getName = function () {
                console.log(this.name);
            }
        }
        Parent.prototype.getColor = function () {
            console.log(this.colors);
        }
        var p1 = new Parent('Leo');
    // 使用Object.create()方法,需传入一个对象
        var c1 = Object.create(p1);
        var c2 = Object.create(p1);
        c1.colors.push('green');
        c1.getName();// Leo
        c1.getColor(); // ["blue", "red", "green"]
        c2.getColor(); // ["blue", "red", "green"]
    

    原型式继承缺点:
    传入的对象(原型)的引用类型属性,会被所有创建出来的实例共享。

    • 寄生式继承
      原理:创建一个仅用于封装继承过程的函数,在该函数内部创建一个新对象,再增强对象,最后返回这个新对象。
      寄生继承的思路跟工厂模式差不多,就是调用一个仅用于封装继承过程的函数。
      寄生继承不用实例化父类,直接实例化一个临时副本实现了原型链继承。
      在该函数内部创建新对象的方法很多:可以使用Object.create(),new或者使用字面量创建。
        function createObj(obj) {
            // 创建新对象()
            var clone = Object.create(obj);
            // 增强这个对象
            clone.sayHi = function() {
                console.log('Hi,'+name);
            }
            // 返回这个对象
            return clone;
        }
    

    寄生式继承缺点:
    无法实现函数复用(封装在一个函数内,没有用到原型,和构造函数模式一样)。

    • 寄生组合式继承
      原理:组合继承的改进版,还是使用原型链来继承原型属性和方法,借用构造函数来继承实例属性和方法。但是,不将子类原型指向父类实例,而是创建一个空构造函数,将父类的原型指向这个空构造函数的原型,将子类的原型指向这个空构造函数的实例。从而实现子类原型间接继承父类原型。
       // 1.模拟Object.create()
        function object(o) {
           function F() {}; // 创建空构造函数
           F.prototype = o; // 将空构造函数的原型指向传入的对象
           return new F();  // 返回构造函数的实例对象,该实例对象继承了传入对象的属性和方法
        }
        // 封装继承函数
       function extend(Child, Parent) {
           // 传入父类的原型,返回的实例obj继承了父类的原型属性和方法
           var obj = object(Parent.prototype);
           // 子类的原型指向obj,子类的原型间接继承了父类的原型属性和方法
           Child.prototype = obj;
           // 修正子类新原型(即obj对象)的constructor,使其指向子类构造函数
           obj.constructor = Child;
       }
    
       // 2.封装成一个函数
       function extend(Child, Parent) {
           var F = function () {};
           // 空构造函数的原型指向父类的原型,相当于复制了一份父类原型
           F.prototype = Parent.prototype;
           // 子类的原型指向空构造函数的实例,相当于子类原型间接继承了父类原型
           Child.prototype = new F();
           // 修正子类原型constructor指向
           Child.prototype.constructor = Child;
       }
    
       // 3.直接使用Object.create()方法
       function extend(Child, Parent) {
           var obj = Object.create(Parent.prototype);
           Child.prototype = obj;
           obj.constructor = Child;
       }
    
    寄生组合继承.png
        function Parent (name) {
            this.name = name;
            this.colors = ['blue', 'red'];
        }
        Parent.prototype.getName = function () {
            console.log(this.name);
        }
        function Child (name, age) {
          // 通过借用构造函数继承父类的实例属性和方法
          Parent.call(this, name);
          this.school = age; // 添加新的子类属性
        }
        // 声明继承函数
        function extend(Child, Parent) {
            var F = function () {};
            F.prototype = Parent.prototype;
            Child.prototype = new F();
            Child.prototype.constructor = Child;
        }
        // 寄生式继承父类原型属性和方法
        extend(Child, Parent);
        var p1 = new Child('Leo', 26);
        var p2 = new Child('Tom', 20);
        p1.colors.push('green');
        p1.getName(); // Leo
        console.log(p1.colors); //["blue", "red", "green"]
        console.log(p2.colors); //["blue", "red"]
    

    参考资料:
    《JavaScript高级程序设计》
    《JavaScript 标准参考教程》

    相关文章

      网友评论

          本文标题:07-继承

          本文链接:https://www.haomeiwen.com/subject/aszbxxtx.html