underscore 诞生记(二)—— 链式调用与混入(mixin)

  • 2019 年 11 月 5 日
  • 筆記

上篇文章讲述了 underscore 的基本结构搭建,本文继续讲链式调用与混入。

如果你还没看过第一篇文章,请点击 “underscore 诞生记(一)—— 基本结构搭建”

链式调用

在 JQuery 中,我们经常使用到链式调用,如:

$('.div')    .css('color', 'red')    .show();

那么在 underscore 中,是否支持链式调用呢?答案是支持的,只不过默认不开启链式调用罢了。

想要实现链式调用,通常我们会在支持链式调用的函数中返回对象本身:

let car = {    run(name) {      console.log(`${name}老司机开车啦喂!`);      return this;    },    stop() {      console.log('车停了');    },  };    car.run('奔驰').stop();    // 奔驰老司机开车啦喂!  // 车停了  复制代码

那么在每个 _ 方法下都 return this , 显然不大优雅缺乏可控性!尝试着写个通用方法 chain() 开启链式调用。

_.chain = function(obj) {    // 获得一个经underscore包裹后的实例    var instance = _(obj);    // 标识当前实例支持链式调用    instance._chain = true;    return instance;  };    // 小试牛刀  _.chain([1, 2, 3]);  /*  {      _chain: true,      _wrapped: [1, 2, 3]  }   */  复制代码

返回的为一个实例对象,后面的方法判断 _chain 属性是否为 true,为 true 的话再调用一次 chain() 方法再返回原来实例即可。我们在之前用于给 prototype 复制方法的 each() 函数加入判断吧

var ArrayProto = Array.prototype;  var push = ArrayProto.push;  _.each(_.functions(_), function(name) {    var func = _[name];    _.prototype[name] = function() {      var args = [this._wrapped];      // args = [this._wrapped, arguments[0], arguments[1]...], 相当于用 this._wrapped 代替 obj 实现      push.apply(args, arguments);      return this._chain ? _(func.apply(_, args)).chain() : func.apply(_, args);    };  });  复制代码

有点冗长,将 return this._chain ? _(func.apply(_, args)).chain() : func.apply(_, args); 改造下,

// 判断是否需要链式调用  var chainResult = function(instance, obj) {    return instance._chain ? _(obj).chain() : obj;  };  var ArrayProto = Array.prototype;  var push = ArrayProto.push;  _.each(_.functions(_), function(name) {    var func = _[name];    _.prototype[name] = function() {      var args = [this._wrapped];      // args = [this._wrapped, arguments[0], arguments[1]...], 相当于用 this._wrapped 代替 obj 实现      push.apply(args, arguments);      return chainResult(this, func.apply(_, args));    };  });  复制代码

好了,试试看效果:

_.chain([1, 2, 3])    .each(function(item) {      console.log(item);    })    .each(function(item) {      console.log(item);    });  // 1 2 3 1 2 3  // {_wrapped: [1,2,3], _chain: true}  复制代码

混入(mixin)

underscore 很强大,功能也很齐全,但有时候也不能满足所有人的需求。我们想创建一些方法,让它挂载在 _ 上,这样我们全局也可以调用到这些方法,作为一款强大的方法库,也应该提供这种接口,让用户自定添加方法,ok, let us do it !

我们先定义一个 mixin 方法

_.mixin = function(obj) {};    // `obj` 为一个类似 `_` 的对象。传入的这个对象,也需要遍历一次,并且复制方法于 prototype 属性上。详细代码如下:  _.mixin = function(obj) {    _.each(_.functions(obj), function(name) {      var func = (_[name] = obj[name]);      _.prototype[name] = function() {        var args = [this._wrapped];        push.apply(args, arguments);        // args = [this._wrapped, arguments[0], arguments[1]...], 相当于用 this._wrapped 代替 obj 实现        return chainResult(this, func.apply(_, args));      };    });    return _;  };  复制代码

看到这里,你会发现,我们在方法的最后遍历赋值给_.prototype方法,其实就是一次mixin() 的调用.

_.each(_.functions(_), function(name) {    var func = _[name];    _.prototype[name] = function() {      var args = [this._wrapped];      // args = [this._wrapped, arguments[0], arguments[1]...], 相当于用 this._wrapped 代替 obj 实现      push.apply(args, arguments);      return func.apply(_, args);    };  });    // 简化为  _.mixin(_);  复制代码

最终代码

(function() {    // root 为挂载对象,为 self 或 global 或 this 或 {}    var root =      (typeof self == 'object' && self.self === self && self) ||      (typeof global == 'object' && global.global === global && global) ||      this ||      {};      var _ = function(obj) {      // 如果传入的是实例后对象,返回它      if (obj instanceof _) return obj;      // 如果还没有实例化,new _(obj)      if (!(this instanceof _)) return new _(obj);      this._wrapped = obj;    };      // 最大数值    var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;    var ArrayProto = Array.prototype;    var push = ArrayProto.push;    // 判断是否为数组    var isArrayLike = function(collection) {      var length = collection.length;      return (        typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX      );    };      // 判断是否需要链式调用    var chainResult = function(instance, obj) {      return instance._chain ? _(obj).chain() : obj;    };      root._ = _;      _.VERSION = '1.9.1'; // 给我们的 underscore 一个版本号吧      /**     * 字符串倒装     */    _.reverse = function(string) {      return string        .split('')        .reverse()        .join('');    };      /**     * 判断是否为 function     */    _.isFunction = function(obj) {      return typeof obj == 'function' || false;    };    // 链式调用方法    _.chain = function(obj) {      // 获得一个经underscore包裹后的实例      var instance = _(obj);      // 标识当前实例支持链式调用      instance._chain = true;      return instance;    };    /**     * 获取_的所有属性函数名     */    _.functions = function(obj) {      var names = [];      for (var key in obj) {        if (_.isFunction(obj[key])) names.push(key);      }      return names.sort();    };    /**     * 数组或对象遍历方法,并返回修改后的对象或数组     * @param iteratee 回调函数     * @param context 回调函数中this的指向     */    _.map = function(obj, iteratee, context) {      var length = obj.length,        results = Array(length);      for (var index = 0; index < length; index++) {        results[index] = iteratee.call(context, obj[index], index, obj);      }        return results;    };      /**     * 数组或对象遍历方法     */    _.each = function(obj, callback) {      var length,        i = 0;        if (isArrayLike(obj)) {        // 数组        length = obj.length;        for (; i < length; i++) {          //   这里隐式的调用了一次 callback.call(obj[i], obj[i], i);          if (callback.call(obj[i], obj[i], i) === false) {            break;          }        }      } else {        // 对象        for (i in obj) {          if (callback.call(obj[i], obj[i], i) === false) {            break;          }        }      }        return obj;    };    /*     * 混入方法 mixin     */    _.mixin = function(obj) {      _.each(_.functions(obj), function(name) {        var func = (_[name] = obj[name]);        _.prototype[name] = function() {          var args = [this._wrapped];          push.apply(args, arguments);          // args = [this._wrapped, arguments[0], arguments[1]...], 相当于用 this._wrapped 代替 obj 实现          return chainResult(this, func.apply(_, args));        };      });      return _;    };    _.mixin(_);  })();  复制代码