underscore 誕生記(二)—— 鏈式調用與混入(mixin)

clipboard.png

上篇文章講述了 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(_);
})();

未完待續,靜待下篇

前端進階小書(advanced_front_end)

前端每日一題(daily-question)

webpack4 搭建 Vue 應用(createVue)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章