Underscore 源碼(一)整體架構

其實,學習一個庫的源碼,最重要的就是先理清它的基本架構,jQuery 是這樣,Underscore 也應該是這樣。javascript

Underscore 這個庫提供力不少有用的函數,這些函數部分已經在 es5 或 es6 中支持了,好比咱們經常使用的 map、reduce、each,還有 es6 中的 keys 方法等,由於這些方法比較好用,因此被 javascript 的制定者採納了。css

先過一遍源碼

我看的版本是 1.8.3,網上不少舊版本的,貌似有不少函數都已經啓用或改變了,有點不同啦。java

打開源碼,會看到函數的基本架構:node

(function(){
  ...
}.call(this))

這和咱們常見的閉包不太同樣啊,可是功能都是相似的,在函數內執行,防止對全局變量進行污染,而後在函數的最後調用 call 函數,把 函數內部的 this 和全局的 this 進行綁定。若是在瀏覽器裏執行,this 會指向 window,在 node 環境下,會指向全局的 global。當厭倦使用閉包的時候,這種方法也是一種不錯的體驗。git

那麼接着向下看:es6

(function(){
  var root = this; // 用 root 來保存當前的 this
  var previousUnderscore = root._; // 萬一 _ 以前被佔用了,先備份

  // 下面是一些原型,包括 數組,對象和函數
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  var
    push             = ArrayProto.push,
    slice            = ArrayProto.slice,
    toString         = ObjProto.toString,
    hasOwnProperty   = ObjProto.hasOwnProperty;

  var
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind,
    nativeCreate       = Object.create;
}.call(this))

在源碼中搜索 previousUnderscore,能夠找到兩處,另一處就是:github

_.noConflict = function() {
  root._ = previousUnderscore;
  return this;
};

noConflict 函數的用法是可讓用戶自定義變量來替代 _,而且把以前保存的 _ 給還原,好比:segmentfault

// 測試使用
var _ = 'Hello World'; // _ 已經被佔用
...

var us = _.noConflict();
us // 指向 underscore
_ // ‘Hello World'

像 push slice 這些函數,原生都已經支持了,源碼裏面直接把他們拿過來使用。數組

來看看 _ 是如何定義的

_ 在 underscore 地位是很是核心的,而它的本質實際上仍是函數,一樣也是一個對象:瀏覽器

var _ = function(obj) {
  if (obj instanceof _) return obj;
  if (!(this instanceof _)) return new _(obj);
  this._wrapped = obj;
};
_.VERSION = '1.8.3';
_.map = _.collect = function(){...};
_.each = _.forEach = function(){...};

_ 是一個函數,可是在源碼中,它是被看成對象來使用,全部的屬性和函數都是直接綁定到 _ 對象上面的,全部最終的調用都是經過:

_.each([22,33,44], console.log);
// 22 0 [22, 33, 44]
// 33 1 [22, 33, 44]
// 44 2 [22, 33, 44]

最終的返回值是處理的那個數組,而不是 _ 本身,下面將會討論,這個涉及到鏈式調用。

那若是,我就想經過函數來生成,這也是支持的:

_([1,2,3]).each(console.log)
// 返回的結果都是同樣的

這個時候,就會疑惑,_ 的原型呢?咱們再來搜索一下 _.prototype

_.mixin = function(obj) {
  _.each(_.functions(obj), function(name) { // 調用 each 對每個函數對象處理
    var func = _[name] = obj[name]; // 綁定到 _ 上
    _.prototype[name] = function() { // 綁定到 _ 的原型上
      var args = [this._wrapped];
      push.apply(args, arguments); // 參數對齊
      return result(this, func.apply(_, args)); // 調用 result 查看是否鏈式
    };
  });
};

_.mixin(_); // 執行

// 相關的一些方法
_.functions = _.methods = function(obj) {
  var names = [];
  for (var key in obj) {
    if (_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
};
_.isFunction = function(obj){
  return typeof obj == 'function' || false;
}

_.functions 是一個獲取目標全部函數對象的方法,並把這些方法淺拷貝傳遞給 __的原型,由於原型方法,處理對象已經在 _wrapped 中了,而這些經常使用的方法參數都是固定的,若是直接調用,參數會出問題,因此:

var args = [this._wrapped];
push.apply(args, arguments);// args 已經拼接完成
func.apply(_, args);

那麼 result 函數是用來作什麼的?由於 underscore 有兩種調用方式,一種是經過 _.each(obj, func),另外一種是經過 _(obj).each(func)。第一種方法很好理解,返回值要麼是 obj 自己,要麼是處理後的結果,而第二種調用方法和 jQuery 很像,先生成一個 new 實體,對實體的進行調用,也就有了上面的參數校準問題。

不過這樣子又迴帶來另外一個問題,對於 each、map 函數,函數返回什麼不重要,主要是處理過程,能夠支持鏈式調用,對於 reduce 函數,返回的是處理後的結果,能夠不用鏈式,因此 result 函數就是來判斷是否須要鏈式,而對返回值進行處理。

介紹 result 以前,先來看一下 chain 函數:

_.chain = function(obj) {
  var instance = _(obj);
  instance._chain = true; // 設置一個 _chain 屬性,後面用於判斷鏈式
  return instance;
};

返回一個新的 _(obj),而且多了一個 _chain 屬性,且爲 true,因此 result 函數:

var result = function(instance, obj) {
  return instance._chain ? _(obj).chain() : obj;
};

若是當前是容許鏈式的,能夠進行鏈式調用,不容許鏈式,就直接返回處理結果,好比:

var arr = [22, 33, 44];
_.chain(arr)
  .map(function(v){ return v + 1 })
  .reduce(function(p, n){ return p + n }, 0)
  .value() // 102

// 若是不容許鏈式,返回結果是處理後的數組
_(arr)
  .map(function(v){ return v + 1 }) // [23, 34, 45]

如今返回來看一下 _ 函數,也很是的有意思,_(obj)其實是執行兩次的,第二次纔用到了 new:

var _ = function(obj) {
  if (obj instanceof _) return obj; // 若是 obj 繼承於 _,直接返回
  if (!(this instanceof _)) return new _(obj); // 若是 this 不繼承 _,返回一個 new
  this._wrapped = obj; // 保存 obj 的值
};

如今應該就很是的明朗了吧。當調用 _([22,33,44]) 的時候,發現 obj 並非繼承與 _,會用 new 來生成,又會從新跑一遍 _ 函數,而後將 _wrapped 屬性指向 obj。

因爲在以前已經 root = this,Underscore 在不一樣的環境中均可以運行,須要將 _ 放到不一樣的環境中:

if (typeof exports !== 'undefined') { // nodejs 模塊
  if (typeof module !== 'undefined' && module.exports) {
    exports = module.exports = _;
  }
  exports._ = _;
} else { // window
  root._ = _;
}

接着看源碼

源碼再往下看,是一個 optimizeCb 函數,用來優化回調函數:

var optimizeCb = function(func, context, argCount) {
  // 這裏沒有用 undefined,而是用 void 0
  if (context === void 0) return func; // 只有一個參數,直接返回回調函數
  switch (argCount == null ? 3 : argCount) { // call 比 apply 好?
    case 1: return function(value) {
      return func.call(context, value);
    };
    case 2: return function(value, other) {
      return func.call(context, value, other);
    };
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }
  // 最後走 apply 函數
  return function() {
    return func.apply(context, arguments);
  };
};

所謂優化版的回調函數,就是用 call 來固定參數,1 個參數,2 個參數,3 個參數,4 個參數的時候,因爲 apply 能夠不用考慮參數,可是在性能上面貌似沒有 call 好。

而後後面還有一個 cb 函數,也是用來做爲回調函數的。

var cb = function(value, context, argCount) {
  if (value == null) return _.identity;
  if (_.isFunction(value)) return optimizeCb(value, context, argCount);
  if (_.isObject(value)) return _.matcher(value);
  return _.property(value);
};
_.iteratee = function(value, context) {
  return cb(value, context, Infinity);
};

iteratee 能夠用來對函數進行處理,給一個函數綁定 this 等等,最總仍是調用到 cb,其實 cb 自己就很複雜,要麼是一個 identity 函數,要麼是一個優化到回調函數,要麼是一個 property 獲取屬性函數。

再往下就是 createAssigner,搜了一下,發現全文有三處用到此函數,分別是 extend、extendOwn、default,能夠看出來,此函數主要到做用是用來實現拷貝,算是拷貝到輔助函數吧,把拷貝公共到部分抽離出來:

var createAssigner = function(keysFunc, undefinedOnly) {
  return function(obj) {
    var length = arguments.length;
    if (length < 2 || obj == null) return obj;

    // 將第二個參數及之後的 object 拷貝到第一個 obj 上
    for (var index = 1; index < length; index++) {
      var source = arguments[index],
          // keysFunc 是點睛所在
          // 不一樣的 keysFunc 得到的 keys 集合不一樣
          // 分爲兩種,全部 keys(包括繼承),自身 keys
          keys = keysFunc(source),
          l = keys.length;
      for (var i = 0; i < l; i++) {
        var key = keys[i];
        // underfinedOnly 表示是否覆蓋原有
        if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
      }
    }
    return obj;
  };
};

因此當 keyFunc 函數得到全部 keys 時,包括繼承來的,這個時候就對應於 _.extend 函數,非繼承 keys 時,對應於 _.extendOwn。若是 underfinedOnly 設置爲 true,則實現的是不替換原有屬性的繼承 _.defaults

在 Underscore 中,原型的繼承用 baseCreate 函數:

var Ctor = function(){};

var baseCreate = function(prototype) {
  if (!_.isObject(prototype)) return {};
  if (nativeCreate) return nativeCreate(prototype);
  Ctor.prototype = prototype;
  var result = new Ctor;
  Ctor.prototype = null;
  return result;
};

nativeCreate 以前已經介紹來,就是 Object.create,因此,若是瀏覽器不支持,下面實現的功能就是在實現這個函數,方法也很常規,用了一個空函數 Ctor 主要是防止 new 帶來的多餘屬性問題。

property 函數也是一個比較有意思的函數,使用了閉包的思路,好比判斷一個對象是否爲相似數組結構的時候就用到了這個函數:

var property = function(key) {
  return function(obj) {
    return obj == null ? void 0 : obj[key];
  };
};

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length'); // 返回一個閉包韓式,用來檢測對象是非有 length 參數
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

並且我搜索了一下,發現 getLength 函數使用的地方仍是挺多的。

總結

總的來講,這些開源的庫,都保持着本身的一種風格,jQuery 是這樣,Underscore 也是這樣,從 Underscore 的整體架構能夠發現,它主要封裝了一些好用的方法。

參考

Underscore.js (1.8.3) 中文文檔
Underscore源碼解析(一)
中文版 underscore 代碼註釋

歡迎來個人博客交流。

相關文章
相關標籤/搜索