• 【编程题与分析题】Javascript 之继承的多种实现方式和优缺点总结


    [!NOTE]
    能熟练掌握每种继承方式的手写实现,并知道该继承实现方式的优缺点。

    原型链继承

        function Parent() {
          this.name = 'zhangsan';
          this.children = ['A', 'B', 'C'];
        }
        Parent.prototype.getName = function() {
          console.log(this.name);
        }
        
        function Child() {
          
        }
        Child.prototype = new Parent();
        var child = new Child();
        console.log(child.getName());
    

    [!NOTE]
    主要问题:
    1. 引用类型的属性被所有实例共享(this.children.push('name'))
    2. 在创建Child的实例的时候,不能向Parent传参

    借用构造函数(经典继承)

        function Parent(age) {
          this.names = ['zhangsan', 'lisi'];
          this.age = age;
          
          this.getName = function() {
            return this.names;
          }
          
          this.getAge = function() {
            return this.age;
          }
        }
        
        function Child(age) {
          Parent.call(this, age);
        }
        var child = new Child(18);
        child.names.push('haha');
        console.log(child.names);
        
        var child2 = new Child(20);
        child2.names.push('yaya');
        console.log(child2.names);
    

    [!NOTE]
    优点:
    1. 避免了引用类型的属性被所有实例共享
    2. 可以直接在Child中向Parent传参
    缺点:
    方法都在构造函数中定义了,每次创建实例都会创建一遍方法

    组合继承(原型链继承和经典继承双剑合璧)

        /**
        * 父类构造函数
        * @param name
        * @constructor
        */
        function Parent(name) {
          this.name = name;
          this.colors = ['red', 'green', 'blue'];
        }
        
        Parent.prototype.getName = function() {
          console.log(this.name);
        }
        
        // child
        function Child(name, age) {
          Parent.call(this, name);
          this.age = age;
        }
        
        Child.prototype = new Parent();
        // 校正child的构造函数
        Child.prototype.constructor = Child;
        
        // 创建实例
        var child1 = new Child('zhangsan', 18);
        child1.colors.push('orange');
        console.log(child1.name, child1.age, child1.colors);    // zhangsan 18 (4) ["red", "green", "blue", "orange"]
        
        var child2 = new Child('lisi', 28);
        console.log(child2.name, child2.age, child2.colors);    // lisi 28 (3) ["red", "green", "blue"]
    

    [!NOTE]
    优点: 融合了原型链继承和构造函数的优点,是Javascript中最常用的继承模式

    ------ 高级继承的实现

    原型式继承

        function createObj(o) {
          function F(){};
          // 关键:将传入的对象作为创建对象的原型
          F.prototype = o;
          return new F();
        }
        
        // test
        var person = {
            name: 'zhangsan',
            friends: ['lisi', 'wangwu']
        }
        var person1 = createObj(person);
        var person2 = createObj(person);
        
        person1.name = 'wangdachui';
        console.log(person1.name, person2.name);  // wangdachui, zhangsan
        
        person1.friends.push('songxiaobao');
        console.log(person2.friends);       // lisi wangwu songxiaobao
    

    [!WARNING]
    缺点:
    对于引用类型的属性值始终都会共享相应的值,和原型链继承一样

    寄生式继承

        // 创建一个用于封装继承过程的函数,这个函数在内部以某种形式来增强对象
        function createObj(o) {
          var clone = Object.create(o);
          clone.sayName = function() {
            console.log('say HelloWorld');
          }
          return clone;
        }
    

    [!WARNING]
    缺点:与借用构造函数模式一样,每次创建对象都会创建一遍方法

    寄生组合式继承

    基础版本

        function Parent(name) {
          this.name = name;
          this.colors = ['red', 'green', 'blue'];
        }
        
        Parent.prototype.getName = function() {
          console.log(this, name);
        }
        
        function Child(name, age) {
          Parent.call(this, name);
          this.age = age;
        }
        
        // test1:
        // 1. 设置子类实例的时候会调用父类的构造函数
        Child.prototype = new Parent();
        // 2. 创建子类实例的时候也会调用父类的构造函数
        var child1 = new Child('zhangsan', 18);   // Parent.call(this, name);
        
        
        // 思考:如何减少父类构造函数的调用次数呢?
        var F = function(){};
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        
        // 思考:下面的这一句话可以吗?
        /* 分析:因为此时Child.prototype和Parent.prototype此时指向的是同一个对象,
                因此部分数据相当于此时是共享的(引用)。
                比如此时增加 Child.prototype.testProp = 1; 
                同时会影响 Parent.prototype 的属性的。
              如果不模拟,直接上 es5 的话应该是下面这样吧
              Child.prototype = Object.create(Parent.prototype);*/
        Child.prototype = Parent.prototype;
        
        // 上面的三句话可以简化为下面的一句话
        Child.prototype = Object.create(Parent.prototype);
        
        
        
        // test2:
        var child2 = new Child('lisi', 24);
    

    优化版本

        // 自封装一个继承的方法
        function object(o) {
          // 下面的三句话实际上就是类似于:var o = Object.create(o.prototype)
          function F(){};
          F.prototype = o.prototype;
          return new F();
        }
        
        function prototype(child, parent) {
          var prototype = object(parent.prototype);
          // 维护原型对象prototype里面的constructor属性
          prototype.constructor = child;
          child.prototype = prototype;
        }
        
        // 调用的时候
        prototype(Child, Parent)
    

    创建对象的方法

    • 字面量创建
    • 构造函数创建
    • Object.create()
    var o1 = {name: 'value'};
    var o2 = new Object({name: 'value'});
    
    var M = function() {this.name = 'o3'};
    var o3 = new M();
    
    var P = {name: 'o4'};
    var o4 = Object.create(P)
    

    原型

    • JavaScript 的所有对象中都包含了一个 __proto__ 内部属性,这个属性所对应的就是该对象的原型
    • JavaScript 的函数对象,除了原型 __proto__ 之外,还预置了 prototype 属性
    • 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型 __proto__

    原型

    原型链

    任何一个实例对象通过原型链可以找到它对应的原型对象,原型对象上面!

    的实例和方法都是实例所共享的。

    一个对象在查找以一个方法或属性时,他会先在自己的对象上去找,找不到时,他会沿着原型链依次向上查找。

    注意: 函数才有prototype,实例对象只有有__proto__, 而函数有的__proto__是因为函数是Function的实例对象

    instanceof原理

    判断实例对象的__proto__属性与构造函数的prototype是不是用一个引用。如果不是,他会沿着对象的__proto__向上查找的,直到顶端Object。

    判断对象是哪个类的直接实例

    使用对象.construcor直接可判断

    构造函数,new时发生了什么?

       var obj  = {}; 
       obj.__proto__ = Base.prototype;
       Base.call(obj);  
    
    1. 创建一个新的对象 obj;
    2. 将这个空对象的__proto__成员指向了Base函数对象prototype成员对象
    3. Base函数对象的this指针替换成obj, 相当于执行了Base.call(obj);
    4. 如果构造函数显示的返回一个对象,那么则这个实例为这个返回的对象。 否则返回这个新创建的对象

    类的声明

    // 普通写法
    function Animal() {
      this.name = 'name'
    }
    
    // ES6
    class Animal2 {
      constructor () {
        this.name = 'name';
      }
    }
    

    继承

    借用构造函数法

    在构造函数中 使用Parent.call(this)的方法继承父类属性。

    原理: 将子类的this使用父类的构造函数跑一遍

    缺点: Parent原型链上的属性和方法并不会被子类继承

    function Parent() {
      this.name = 'parent'
    }
    
    function Child() {
      Parent.call(this);
      this.type = 'child'
    }
    

    原型链实现继承

    原理:把子类的prototype(原型对象)直接设置为父类的实例

    缺点:因为子类只进行一次原型更改,所以子类的所有实例保存的是同一个父类的值。
    当子类对象上进行值修改时,如果是修改的原始类型的值,那么会在实例上新建这样一个值;
    但如果是引用类型的话,他就会去修改子类上唯一一个父类实例里面的这个引用类型,这会影响所有子类实例

    function Parent() {
      this.name = 'parent'
      this.arr = [1,2,3]
    }
    
    function Child() {
      this.type = 'child'
    }
    
    Child.prototype = new Parent();
    var c1 = new Child();
    var c2 = new Child();
    c1.__proto__ === c2.__proto__
    

    组合继承方式

    组合构造函数中使用call继承和原型链继承。

    原理: 子类构造函数中使用Parent.call(this);的方式可以继承写在父类构造函数中this上绑定的各属性和方法;
    使用Child.prototype = new Parent()的方式可以继承挂在在父类原型上的各属性和方法

    缺点: 父类构造函数在子类构造函数中执行了一次,在子类绑定原型时又执行了一次

    function Parent() {
      this.name = 'parent'
      this.arr = [1,2,3]
    }
    
    function Child() {
      Parent.call(this);
      this.type = 'child'
    }
    
    Child.prototype = new Parent();
    

    组合继承方式 优化1:

    因为这时父类构造函数的方法已经被执行过了,只需要关心原型链上的属性和方法了

    Child.prototype = Parent.prototype;
    

    缺点:

    • 因为原型上有一个属性为constructor,此时直接使用父类的prototype的话那么会导致 实例的constructor为Parent,即不能区分这个实例对象是Child的实例还是父类的实例对象。
    • 子类不可直接在prototype上添加属性和方法,因为会影响父类的原型

    注意:这个时候instanseof是可以判断出实例为Child的实例的,因为instanceof的原理是沿着对象的__proto__判断是否有一个原型是等于该构造函数的原型的。这里把Child的原型直接设置为了父类的原型,那么: 实例.proto === Child.prototype === Child.prototype

    组合继承方式 优化2 - 添加中间对象【最通用版本】:

    function Parent() {
      this.name = 'parent'
      this.arr = [1,2,3]
    }
    
    function Child() {
      Parent.call(this);
      this.type = 'child'
    }
    
    Child.prototype = Object.create(Parent.prototype); //提供__proto__
    Child.prototype.constrctor = Child;
    

    Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

    创建JS对象的多种方式总结

    工厂模式

     
        /**
        * 工厂模式创建对象
        * @param name
        * @return {Object}
        */
        function createPerson(name){
            var o = new Object();
            o.name = name;
            o.getName = function() {
              console.log(this.name);
            }
            return o;
        }
        var person = createPerson('zhangsan');
        console.log(person.__proto__ === Object.prototype); // true
    

    缺点:无法识别当前的对象,因为创建的所有对象实例都指向的是同一个原型

    构造函数模式

    构造函数创建对象基础版本

        /**
        * 使用构造函数的方式来创建对象
        * @param name
        * @constructor
        */
        function Person(name) {
          this.name = name;
          this.getName = function() {
            console.log(this.name)
          }
        }
        var person = new Person('lisi');
        console.log(person.__proto__ === Person.prototype)
    

    优点:实例剋识别伪一个特定的类型
    缺点:每次创建实例对象的时候,每个方法都会被创建一次

    构造函数模式优化

        function Person(name) {
          this.name = name;
          this.getName = getName;
        }
        
        function getName() {
          console.log(this.name);
        }
        
        var person = new Person('zhangsan');
        console.log(person.__proto__ === Person.prototype);
    

    优点:解决了每个方法都要被重新创建的问题
    缺点:不合乎代码规范……

    原型模式

    原型模式基础版

        function Person(name) {
          
        }
        Person.prototype.name = 'lisi';
        Person.prototype.getName = function() {
          console.log(this.name);
        }
        var person = new Person();
        console.log(Person.prototype.constructor)       // Person
    

    优点:方法不会被重新创建
    缺点:1. 所有的属性和方法所有的实例上面都是共享的;2. 不能初始化参数

    原型模式优化版本一

        function Person(name) {
          
        }
        Person.prototype = {
            name: 'lisi',
            getName: function() {
              console.log(this.name);
            }
        }
        var person = new Person();
        console.log(Person.prototype.constructor)       // Object
        console.log(person.constructor == person.__proto__.constructor) // true
    

    优点:封装性好了一些
    缺点:重写了Person的原型prototype属性,丢失了原始的prototype上的constructor属性

    原型模式优化版本二

        function Person(name) {
          
        }
        Person.prototype = {
            constructor: Person,
            name: 'lisi',
            getName: function() {
              console.log(this.name)
            }
        }
        var person = new Person();
    

    优点:实例可以通过constructor属性找到所属的构造函数
    缺点:所有的属性和方法都共享,而且不能初始化参数

    组合模式

        function Person(name) {
          this.name = name;
        }
        Person.prototype = {
            constructor: Person,
            getName: function() {
              console.log(this.name)
            }
        }
        var person = new Person('zhangsan');
    

    优点:基本符合预期,属性私有,方法共享,是目前使用最广泛的方式
    缺点:方法和属性没有写在一起,封装性不是太好

    动态原型模式

        // 第一种创建思路:
        function Person(name) {
           this.name = name;
           if (typeof this.getName !== 'function') {
               Person.prototype.getName = function() {
                 console.log(this.name);
               }
           }
        }
        var person = new Person();
    
        // 第二种创建的思路:使用对象字面量重写原型上的方法
        function Person(name) {
          this.name = name;
          if (typeof this.getName !== 'function') {
              Person.prototype = {
                  constructor: Person,
                  getName: function() {
                    console.log(this.name)
                  }
              }
              return new Person(name);
          }
        }
        
        var person1 = new Person('zhangsan');
        var person2 = new Person('lisi');
        console.log(person1.getName());
        console.log(person2.getName());
        
    

    寄生构造函数模式

        /**
        * 寄生构造函数模式
        * @param name
        * @return {Object}
        * @constructor
        */
       function Person(name){
            var o = new Object();
            o.name = name;
            o.getName = function() {
              console.log(this.name)
            }
            return o;
       }
       var person = new Person('zhangsan');
       console.log(person instanceof Person);   // false
       console.log(person instanceof Object);   // true
       
       
       // 使用寄生-构造函数-模式来创建一个自定义的数组
       /**
        * 特殊数组的构造器
        * @constructor
        */
       function SpecialArray() {
         var values = new Array();
         /*for (var i = 0, len = arguments.length; i < len; i++) {
             values.push(arguments[i]);
         }*/
         // 开始添加数据(可以直接使用apply的方式来优化代码)
         values.push.apply(values, arguments);
         
         // 新增的方法
         values.toPipedString = function(){
             return this.join('|');
         }
         
         return values;
       }
       
       // 使用new来创建对象
       var colors1 = new SpecialArray('red1', 'green1', 'blue1');
       // 不使用new来创建对象
       var colors2 = SpecialArray('red2', 'green2', 'blue2');
       
       console.log(colors1, colors1.toPipedString());
       console.log(colors2, colors2.toPipedString());
    

    稳妥构造函数模式

        /**
        * 稳妥的创建对象的方式
        * @param name
        * @return {number}
        * @constructor
        */
        function Person(name){
            var o = new Object();
            o.sayName = function() {
               // 这里有点类似于在一个函数里面使用外部的变量
               // 这里直接输出的是name
              console.log(name);
            }
            return o;
        }
        var person =  Person('lisi');
        person.sayName();
        person.name = 'zhangsan';
        person.sayName();
        console.log(person instanceof Person);      // false
        console.log(person instanceof Object);      // false
    

    [!NOTE]
    与寄生的模式的不同点:1. 新创建的实例方法不引用this 2.不使用new操作符调用构造函数
    优点:最适合一些安全的环境中使用
    缺点:和工厂模式一样,是无法识别对象的所属类型的

  • 相关阅读:
    【Spring Security学习之一】Spring Security入门
    【OAuth 2.0学习之一】OAuth 2.0入门
    【JWT学习之二】JWT实现单点登录
    【JWT学习之一】JWT入门
    【CAS学习之三】CAS客户端验证
    【CAS学习之二】部署CAS服务端
    【CAS学习之一】CAS入门
    ES 聚合分类(text字段)语句报错
    LeNet网络在fashion MNIST数据集上的实现
    动手深度学习 pytorch
  • 原文地址:https://www.cnblogs.com/fecommunity/p/11908891.html
Copyright © 2020-2023  润新知