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高级程序设计引发的一些思考。因此,百忙之中,我们也需要经常去温习基础知识,所谓温故而知新,正是如此。

  • 相关阅读:
    首次使用随便写点哦
    js中call、apply和bind的区别
    前端的事件流以及事件处理程序
    javascript中数组的深拷贝的方法
    我的第一篇博客
    圆盘转动按钮-react native
    鼠标拖拽删除
    js基础 -----鼠标事件(按下 拖拽)
    清除浮动的几种常用方法
    VUE常见问题解决
  • 原文地址:https://www.cnblogs.com/danceonbeat/p/10704792.html
Copyright © 2011-2022 走看看