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('Benz').stop();    // Benz老司機開車啦喂!  // 車停了  複製程式碼

那麼在每個 _ 方法下都 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(_);  })();  複製程式碼