• home > webfront > ECMAS > emphasis >

    JavaScript继承的实现方式:原型语言对象继承对象原理剖析

    Author:zhoulujun Date:

    在经典的面向对象语言中,可能倾向于定义类,继承类。原型语言 只有对象,没有类;对象继承对象,而不是类继承类。继承的对象函数并不是通过复制而来,而是通过原型链继承。JavaScript里面没有类这个概念,es6中class虽然很像

    面向对象编程:继承、封装、多态。

    对象的继承:A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。

    在经典的面向对象语言中,您可能倾向于定义类对象,然后您可以简单地定义哪些类继承哪些类(参考C++ inheritance里的一些简单的例子),JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承

    回顾《再谈javascriptjs原型与原型链及继承相关问题

    什么是原型语言 

    1. 只有对象,没有类;对象继承对象,而不是类继承类。 

    2. “原型对象”是核心概念。原型对象是新对象的模板,它将自身的属性共享给新对象。一个对象不但可以享有自己创建时和运行时定义的属性,而且可以享有原型对象的属性。 

    3. 每一个对象都有自己的原型对象,所有对象构成一个树状的层级系统。root节点的顶层对象是一个语言原生的对象,只有它没有原型对象,其他所有对象都直接或间接继承它的属性。 

    原型语言创建有两个步骤 

    1. 使用”原型对象”作为”模板”生成新对象 :这个步骤是必要的,这是每个对象出生的唯一方式。以原型为模板创建对象,这也是”原型”(prototype)的原意。 

    2. 初始化内部属性 :这一步骤不是必要的。通俗点说,就是,对”复制品”不满意,我们可以”再加工”,使之获得不同于”模板”的”个性”。 

    所以在JavaScript的世界里,万物皆对象这个概念从一而终。

    JavaScript里面没有类这个概念,es6中class虽然很像类,但实际上只是es5上语法糖而已

    function People(name){
      //属性
      this.name  = name || Annie
      //实例方法
      this.sleep=function(){
        console.log(this.name + '正在睡觉')
      }
    }
    //原型方法
    People.prototype.eat = function(food){
      console.log(this.name + '正在吃:' + food);
    }

    JavaScript的基础方式,首推的就是原型继承

    原型链继承

    父类的实例作为子类的原型

    function Woman(){ 
        this.name= "SubType"; // 子类属性
    }
    // 如果此处有Woman的原型对象上的方法,由于原型重定向,下面的代码会覆盖此方法
    Woman.prototype= new People();// 重写原型对象,代之以一个新类型的实例
    // 这里实例化一个 People时, 实际上执行了两步
    // 1,新创建的对象复制了父类构造函数内的所有属性及方法
    // 2,并将原型 __proto__ 指向了父类的原型对象
    Woman.prototype.name = 'haixia';//子原型的属性
    Woman.prototype.name = ()=>{};//子原型方法
    let womanObj = new Woman();

    原型链继承优点

    • 父类新增原型方法/原型属性,子类都能访问到

    • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例

    • 简单易于实现

    原型链继承缺点:

    1. 可以在子类中增加实例属性,如果要新增加原型属性和方法需要在new 父类构造函数的后面

    2. 无法实现多继承

    3. 来自原型对象的所有属性被所有实例共享,子类可以重写父类原型上的方法

    4. 创建子类实例时,不能向父类构造函数中传参数

    解释原型重定向: Woman.prototype= new People();

    • 自己开辟的堆内存中没有constructor属性,导致类的原型构造函数缺失(解决:自己手动在堆内存中增加constructor属性)

    • 当原型重定向后,浏览器默认开辟的那个原型堆内存会被释放掉,如果之前已经存储了一些方法或者属性,这些东西都会丢失(所以:内置类的原型不允许重定向到自己开辟的堆内存,因为内置类原型上自带很多属性方法,重定向后都没了,这样是不被允许的)

    原型继承经典笔试题

    function Parent () {
      this.a = 1;
      this.b = [1, 2, this.a];
      this.c = {demo: 5};
      this.show = function () {
        console.log(this.a + ' ' + this.c.demo + ':' + this.b + '\n');
      };
    }
    
    function Child () {
      this.a = 2;
      this.change = function () {
        this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
      };
    }
    
    Child.prototype = new Parent();
    var parent = new Parent();
    var child1 = new Child();
    var child2 = new Child();
    child1.a = 11;
    child2.a = 12;
    child1.change();
    child2.change();
    parent.show();
    child1.show();
    child2.show();

    思考原型公用问题


    经典继承

    function extendObj(obj) {
      if (Object.create) {
        return Object.create(obj)
      } else {
        function F () { } 
        F.prototype = obj; 
        return new F()
      }
    }
    var obj = { name: "smd", age: 26, sayHi: function () { } }
    var newObj = createObj(obj)
    /* Extend Function */  
    function extend(subClass,superClass){  
        var Func = function(){} ;  
        Func.prototype = superClass.prototype ;  
        subClass.prototype = new Func() ;  
        subClass.prototype.constructor = subClass ;  
    } ;

    newObj继承了obj的属性和方法,但是同样出现了共享父类中引用类型属性的问题

    实例继承(原型式继承)

    function Wonman(name){
      let instance = new People();
      instance.name = name || 'wangxiaoxia';
      return instance;
    }
    let wonmanObj = new Wonman();
    
    // 父类
    function People (name) {
      this.colors = ["red", "blue", "green"];
      this.name = name; // 父类属性
    }
    People.prototype.sayName = function () { // 父类原型方法
      return this.name;
    };
    
    /** 第一步 */
    // 子类,通过 call 继承父类的实例属性和方法,不能继承原型属性/方法
    function Woman (name, subName) {
      People.call(this, name); // 调用 People 的构造函数,并向其传参 
      this.subName = subName;
    }
    
    /** 第二步 */
    // 解决 call 无法继承父类原型属性/方法的问题
    // Object.create 方法接受传入一个作为新创建对象的原型的对象,创建一个拥有指定原型和若干个指定属性的对象
    // 通过这种方法指定的任何属性都会覆盖原型对象上的同名属性
    Woman.prototype = Object.create(People.prototype, {
      constructor: { // 注意指定 Woman.prototype.constructor = Woman
        value: Woman,
        enumerable: false,
        writable: true,
        configurable: true
      },
      run : {
        value: function(){ // override
          People.prototype.run.apply(this, arguments);
          // call super
          // ...
        },
        enumerable: true,
        configurable: true,
        writable: true
      }
    })
    
    /** 第三步 */
    // 最后:解决 Woman.prototype.constructor === People 的问题
    // 这里,在上一步已经指定,这里不需要再操作
    //Woman.prototype.constructor = Woman;
    
    var instance = new Woman('An', 'sistenAn')


    实例继承优点:

    • 不限制调用方式

    • 简单,易实现

    实例继承缺点:

    • 不能多次继承

    拷贝继承

    function Wonman (name) {
      let instance = new People();
      for (var p in instance) {
        Wonman.prototype[p] = instance[p];
      }
      Wonman.prototype.name=name||'Tom'
    }
    
    let wonmanObj = new Wonman();

    特点:

    • 支持多继承

    缺点:

    • 效率较低,内存占用高(因为要拷贝父类的属性)

    • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

    对象冒充继承

    function Woman(name, age) {
      //3行关键代码 此三行用于获取父类的成员及方法
      //用子类的this去冒充父类的this,实现继承
      //父类People中的this.name、sleep,分别成为了子类的成员、子类的方法
      this.method = People;
      //接收子类的参数 传给父类
      this.method(name);
      //删除父类
      delete this.method;
    
      //此后的this均指子类
      this.age = age;
      this.sayWorld = function() {
        alert(age);
      }
    }

    因为对象冒充的留下,才有call apply的兴起


    借用构造函数继承(伪造对象、经典继承)

    复制父类的实例属性给子类

    • 函数只不过是在特定环境中执行代码的对象,所以这里使用 apply/call 来实现。

    • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

    function Woman(name){
     //继承了People,子类的this传给父类
      People.call(this); //People.call(this,'zhoulujun'); 
      this.name = name || 'andy'
    }
    let womanObj = new Woman();

    通过这种调用,把父类构造函数的this指向为子类实例化对象引用,从而导致父类执行的时候父类里面的属性都会被挂载到子类的实例上去。

    但是通过这种方式,父类原型上的东西是没法继承的,因此函数复用也就无从谈起


    Woman无法继承Parent的原型对象,并没有真正的实现继承(部分继承)

    借用构造函数继承优点:

    1. 解决了子类构造函数向父类构造函数中传递参数

    2. 解决了子类实例共享父类引用属性的问题

    3. 可以实现多继承(call或者apply多个父类)

    借用构造函数继承缺点:

    1. 方法都在构造函数中定义,无法复用

    2. 不能继承原型属性/方法,只能继承父类的实例属性和方法


    组合式继承

    调用父类构造函数,继承父类的属性,通过将父类实例作为子类原型,实现函数复用

    function Woman(name,age){
      People.call(this,name,age)
    }
    Woman.prototype = new People();
    Woman.prototype.constructor = Woman;
    let wonmanObj = new Woman(ren,27);
    wonmanObj.eat();


    缺点:

    • 由于调用了两次父类,所以产生了两份实例

      • 第一次是 Woman.prototype = new People()

      • 第二次是 在实例化的时候,People.call(this,name,age)

    优点:

    • 函数可以复用

    • 不存在引用属性问题

    • 可以继承属性和方法,并且可以继承原型的属性和方法


    寄生组合继承

    通过寄生的方式来修复组合式继承的不足,完美的实现继承

    function Woman(name,age){
      //继承父类属性
      People.call(this,name,age)
    }
    //继承父类方法,可以简化为:Woman.prototype = Object.create(People.prototype);
    (function(){
      // 创建空类
      function Super (){};
      Super.prototype = People.prototype;
      //父类的实例作为子类的原型
      Woman.prototype = new Super();
    })();
    //修复构造函数指向问题
    Woman.prototype.constructor = Woman;
    let womanObj = new Woman();

    其实还是有两次执行


    es6继承

    //class 相当于es5中构造函数
    //class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
    //class中定义的所有方法是不可枚举的
    //class中只能定义方法,不能定义对象,变量等
    //class和方法内默认都是严格模式
    //es5中constructor为隐式属性
    class People{
      constructor(name='wang',age='27'){
        this.name = name;
        this.age = age;
      }
      eat(){
        console.log(`${this.name} ${this.age} eat food`)
      }
    }
    // 继承父类属性,super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
    // 子类必须在constructor方法中调用super方法,否则新建实例时会报错。如果子类没有定义constructor方法,这个方法会被默认添加,不管有没有显式定义,任何一个子类都有constructor方法。
    class Woman extends People{ 
       constructor(name = 'ren',age = '27'){ 
         super(name, age); 
       } 
        eat(){ 
         //继承父类方法
          super.eat() 
        } 
    } 
    let wonmanObj=new Woman('xiaoxiami'); 
    wonmanObj.eat();

    ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

    ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

    /**
     * 继承
     * @param {*} subClass 子类
     * @param {*} superClass 父类
     */
    function _inherits(subClass, superClass) {
      // 类型检测
      if (!superClass || typeof superClass !== "function" ) {
        throw new TypeError("Super expression must either be null or a function, not " +typeof superClass);
      }
      /**
       * Object.create 接受两个参数
       * 指定原型创建对象
       * @param {*} 目标原型
       * @param {*} 添加属性
       */
      subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
          value: subClass, // subClass.prototype.constructor 指向 subClass
          enumerable: false, // constructor 不可枚举
          writable: true,
          configurable: true
        }
      });
    
      /**
       * Object.setPrototypeOf 方法
       * 设置子类的 __proto__ 属性指向父类
       * @param {*} 子类
       * @param {*} 父类
       */
      if (superClass) {
        // 设置子类的__proto__ 让 Child 能访问父类静态属性
        Object.setPrototypeOf
          ? Object.setPrototypeOf(subClass, superClass)
          : (subClass.__proto__ = superClass);
      }
    }



    参考文章:

    JavaScript深入之继承的多种方式和优缺点 #16 https://github.com/mqyqingfeng/Blog/issues/16

    JavaScript 中的继承 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance

    JavaScript常见的六种继承方式 https://segmentfault.com/a/1190000016708006

    js继承的几种方式 https://zhuanlan.zhihu.com/p/37735247

    深入浅出js实现继承的7种方式 https://cloud.tencent.com/developer/article/1536957

    前端面试必备之JS继承方式总结 https://www.imooc.com/article/20162




    转载本站文章《JavaScript继承的实现方式:原型语言对象继承对象原理剖析》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2020_0629_8494.html