zoukankan      html  css  js  c++  java
  • 关于js中原生构造函数的继承

    前言

    在如今快节奏的工作当中,很多基础的东西会渐渐地被丢掉。就如继承这个话题,写React的同学应该都是class xxx extends React.Component,然而这可以理解为es5的一个语法糖,所以问题又回到了js如何实现继承。面试结束后,赶紧翻了翻积满灰尘的js高级程序设计,重新学习了一遍面向对象这一章,有一个创建对象的模式吸引到了我。

    寄生构造函数模式

    在oo中我们是通过类去创建自定义类型的对象,然而js中没有类的概念,在es5的时代,如果我们要去模拟类,学过的同学应该知道最好采用一种构造函数与原型混成的模式。而书中作者提到了一种有意思的模式,叫做寄生构造函数模式,代码如下:

    function Person(name, age, job) {
        var o = new Object();
        o.name = name;
        o.age = age;
        o.job = job;
        o.sayName = function() {
            alert(this.name);
        };
        return o;
    }
    var friend = new Person("Nicholas", 29, "Software Engineer");
    friend.sayName(); // "Nicholas"
    

    对于这种模式有诸多不解:

    1. 仔细一看,这特么不就是所谓的工厂函数模式吗?工厂模式的几个缺点它都存在,一种是创建的所有对象均为Object类型,无法进行类型识别;其次每次创建对象都会重新生成一个function用来创建sayName属性,浪费内存。
    2. 这里的new有什么意义吗?new的作用是生成一个对象,将当前上下文即this指向该对象,然后return该对象。但是此处return了一个o,new就完全没用了。
      带着诸多的不解,又看到了作者提到了该模式的一个使用场景,看代码:
    function SpecialArray() {
        // 创建数组
        var values = new Array();
        // 添加值
        values.push.apply(values, arguments);
        // 添加方法
        values.toPipedString = function() {
            return this.join("|");
        };
        // 返回数组
        return values;
    }
    var colors = new SpecialArray("red", "blue", "green");
    alert(colors.toPipedString()); // "red|blue|green"
    

    从代码我们得知,该构造函数是希望创建一个具有额外方法的特殊数组,仔细想想,这不就是继承嘛。继承在书中提到的最棒的方式是通过寄生组合式继承,那为什么还要通过这种方式来实现Array继承,况且该方式有个很大的问题就是上面提到的类型无法通过instanceof来确定。

    寄生组合式继承

    我们先来看看最常用的继承范式:寄生组合式继承,写法如下:

    function SpecialArray() {
      // 调用Array函数,绑定给当前上下文
      Array.apply(this, arguments);
    };
    
    // 创建一个以Array.prototype为原型的对象作为SpecialArray的原型
    SpecialArray.prototype = Object.create(Array.prototype);
    
    // constructor指向SpecialArray,默认情况[[enumerable]]为false
    Object.defineProperty(SpecialArray.prototype, "constructor", {
      enumerable: false,
      value: SpecialArray
    });
    
    SpecialArray.prototype.toPipedString = function() {
      return this.join("|");
    };
    
    var arr = new SpecialArray(1, 2, 3);
    
    console.log(arr); // arr为SpecialArray {}
    console.log(new Array(1, 2, 3).hasOwnProperty('length')) // true 证明length是Array的实例属性
    console.log(arr.hasOwnProperty('length')) // false 证明Array无视apply方法的this绑定
    

    上面是典型的寄生组合式继承的写法,其存在几个问题:

    1. new的行为上面介绍过,它会返回对象类型,而我们的SpecialArray希望像Array一样,new的时候返回数组。
    2. 我们先通过hasOwnProperty证明了length是Array的一个实例属性,既然如此通过执行Array.apply(this, arguments)会将length绑定给SpecialArray的实例arr,但是实际arr上没有length属性,因此可以证明Array无视apply方法的this绑定。

    既然this无法绑定,那我们只能通过new一个Array来帮我们构造一个数组实例并返回,此时我们的构造函数应该像这样:

    function SpecialArray() {
      var values = new Array()
      // 添加初始值
      values.push.apply(values, arguments);
      return values
    };
    

    这其实就是我们上面提到的寄生构造函数模式,但是此时返回的values是Array的实例,其原型对象是Array.prototype。这样会造成两个问题:

    1. 无法通过instanceof确定实例的类型,它始终为Array的实例
    2. 我们希望将构造函数的方法放入prototype实现共享,而不是放入构造函数中,在每次生成实例都重新生成一个function

    因此我们要做的事情就是将生成的values实例的原型指向SpecialArray.prototype。我们知道实例对象有一个__proto__属性,它指向其构造函数的原型,我们可以通过修改该属性达到我们的目的:

    function SpecialArray() {
      var values = new Array()
      // 添加初始值
      values.push.apply(values, arguments);
      // 将values的原型指向SpecialArray.prototype
      values.__proto__ = SpecialArray.prototype
      return values
    };
    
    // 创建一个以Array.prototype为原型的对象作为SpecialArray的原型
    SpecialArray.prototype = Object.create(Array.prototype);
    
    // constructor指向SpecialArray,默认情况[[enumerable]]为false
    Object.defineProperty(SpecialArray.prototype, "constructor", {
      enumerable: false,
      value: SpecialArray
    });
    
    SpecialArray.prototype.toPipedString = function() {
      return this.join("|");
    };
    
    var arr = SpecialArray(1, 2, 3); // 不需要new
    
    console.log(arr.toPipedString()); // 1|2|3
    console.log(arr instanceof SpecialArray) // true
    

    我们看到arr.toPipedString()可以返回正确的值了,且arr instanceof SpecialArray为true,即完成了继承。这种做法恰好和原型链继承相反,原型链继承是将父类实例作为子类的原型,而该方法是将父类实例的原型指针指向了子类的原型。但是,这种方法有一个很大的问题:__proto__属性是一个非标准属性,其在部分安卓机上未被实现,因此就有一种说法:ES5及以下的JS无法完美继承数组。

    es6 extends

    es6的extends其实能够很方便的帮我们完成Array继承:

    class SpecialArray extends Array {
      constructor(...args) {
        super(...args)
      }
      
      toPipedString() {
        return this.join("|");
      }
    }
    
    var arr = new SpecialArray(1, 2, 3)
    
    console.log(arr.toPipedString()) // 1|2|3
    console.log(arr instanceof SpecialArray) // true
    

    因为我们调用super的时候是先新建父类的实例this,然后再用子类的构造函数SpecialArray来修饰this,这是es5当中做不到的一点。

    vue中的数组

    我们知道在vue中,push、pop、splice等方法可以触发响应式更新,而arr[0] = 1这种写法无法触发,原因是defineProperty无法劫持数组类型的属性,那么vue是如何让常用的方法触发更新的呢,我们看:

    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    
    var methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ];
    
    /**
     * Intercept mutating methods and emit events
     */
    methodsToPatch.forEach(function (method) {
      // cache original method
      var original = arrayProto[method];
      def(arrayMethods, method, function mutator () {
        var args = [], len = arguments.length;
        while ( len-- ) args[ len ] = arguments[ len ];
    
        var result = original.apply(this, args);
        var ob = this.__ob__;
        var inserted;
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args;
            break
          case 'splice':
            inserted = args.slice(2);
            break
        }
        if (inserted) { ob.observeArray(inserted); }
        // notify change
        ob.dep.notify();
        return result
      });
    });
    

    这是vue的部分源码,我们不用细看,看重点即可。我们可以看到vue创建了一个对象arrayMethods,它是以Array.prototype作为原型的。然后改写了arrayMethods中的push、pop、shift等方法,即在原有功能的基础上触发ob.dep.notify()完成更新。那它是如何将我们声明的数组指向arrayMethods的呢,我们继续看:

    var Observer = function Observer (value) {
      this.value = value;
      this.dep = new Dep();
      this.vmCount = 0;
      def(value, '__ob__', this);
      if (Array.isArray(value)) {
        var augment = hasProto
          ? protoAugment
          : copyAugment;
        augment(value, arrayMethods, arrayKeys);
        this.observeArray(value);
      } else {
        this.walk(value);
      }
    };
    /**
     * Augment an target Object or Array by intercepting
     * the prototype chain using __proto__
     */
    function protoAugment (target, src, keys) {
      /* eslint-disable no-proto */
      target.__proto__ = src;
      /* eslint-enable no-proto */
    }
    
    /**
     * Augment an target Object or Array by defining
     * hidden properties.
     */
    /* istanbul ignore next */
    function copyAugment (target, src, keys) {
      for (var i = 0, l = keys.length; i < l; i++) {
        var key = keys[i];
        def(target, key, src[key]);
      }
    }
    

    我们看到vue先是做了个判断,即当前运行环境是否支持__proto__属性。若支持,执行protoAugment(),将target的__proto__指向arrayMethods,这其实就是我们上面实现的es5的继承方式。若不支持,就将arrayMethods里的方法注入到target中完成mixin的操作。

    总结

    寄生组合式继承虽然很完美,但是它没办法做到继承原生类型的构造函数,此时可以借用我们实现的进化版的寄生构造函数模式完成继承。每个阶段回头去看一些基础总会发现有不同的收获,这次的分享内容也是看了js高级程序设计引发的一些思考。因此,百忙之中,我们也需要经常去温习基础知识,所谓温故而知新,正是如此。

  • 相关阅读:
    hdu 1199 Color the Ball 离散线段树
    poj 2623 Sequence Median 堆的灵活运用
    hdu 2251 Dungeon Master bfs
    HDU 1166 敌兵布阵 线段树
    UVALive 4426 Blast the Enemy! 计算几何求重心
    UVALive 4425 Another Brick in the Wall 暴力
    UVALive 4423 String LD 暴力
    UVALive 4872 Underground Cables 最小生成树
    UVALive 4870 Roller Coaster 01背包
    UVALive 4869 Profits DP
  • 原文地址:https://www.cnblogs.com/danceonbeat/p/10704792.html
Copyright © 2011-2022 走看看