underscore源碼學習(一)

前言

最近在社區瀏覽文章的時候,看到了一位大四學長在尋求前端工做中的面經,看完不得不佩服,掌握知識點真是全面,不管是前端後臺仍是其餘,都有涉獵。javascript

在他寫的文章中,有這麼一句話,大概意思是,沒有看過一個庫或者框架的源碼還敢出來混。而後本身心虛了一下,一直以來,都只是學習使用框架或庫,或者在過程當中有學習框架的思想,但並不深刻。例如,在學習Vue.js中,我曾經去探索過Vue中的雙向綁定是如何實現的,經過什麼模式,什麼API,做者的思想是什麼,也曾經實現過簡單版的雙向綁定。
<!--more-->
可是感受本身在這方面並無什麼提升,尤爲在原生JavaScript的學習中,一些不經常使用的API常常忘,思惟也不夠好。因此有了學習優秀的庫的源碼的想法,一方面可以學習做者的思想,提升本身的分析能力,另外一方面我以爲若是能好好分析一個庫的源碼,對本身的提高也是有的。前端

因此,剛開始,我從源碼比較短的underscore.js(包含註釋只有1.5k行)開始學習起。java

什麼是underscore

Underscore一個JavaScript實用庫,提供了一整套函數式編程的實用功能,可是沒有擴展任何JavaScript內置對象。它是這個問題的答案:「若是我在一個空白的HTML頁面前坐下, 並但願當即開始工做, 我須要什麼?「...它彌補了部分jQuery沒有實現的功能,同時又是Backbone.js必不可少的部分。——摘自Underscore中文文檔node

個人學習之路是基於Underscore1.8.3版本開始的。編程

// Current version.
 _.VERSION = '1.8.3';

做用域包裹

與其餘第三方庫同樣,underscore最外層是一個當即執行函數(IIFE),來包裹本身的業務邏輯。通常使用IIFE有以下好處,能夠建立一個獨立的沙箱似的做用域,避免全局污染,還能夠防止其餘代碼對該函數內部形成影響。(但凡在當即執行函數中聲明的函數、變量等,除非是本身想暴露,不然絕無可能在外部得到)數組

(function(){

    // ...執行邏輯
    
}.call(this))

學習的點,當咱們要寫本身的庫或者封裝某個功能函數時,能夠給本身的庫或函數在最外層包裹一個當即執行函數,這樣既不會受外部影響,也不會給外部添麻煩。瀏覽器

_對象

underscore有下劃線的意思,因此underscore經過一個下劃線變量_來標識自身,值得注意的是,_是一個函數對象或者說是一個構造函數,而且支持無new調用的構造的函數,全部API都會掛載在這個對象上,如_.each,_.map緩存

var _ = function(obj) {
    if(obj instanceof _) return obj;
    if(!(this instanceof _)) return new _(obj) //實例化
    this._wrapped = obj
}

全局命名空間

underscore使用root變量保存了全局的this安全

var root = this;

爲了防止其餘庫對_的衝突或影響,underscore作了以下處理,app

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

執行環境判斷

underscore 既可以服務於瀏覽器,又可以服務於諸如 nodejs 所搭建的服務端。
通常,在客戶端(瀏覽器)環境中,_即爲window._=_,暴露在全局中。若在node環境中,_將被做爲模塊導出,而且向後兼容老的API,即require。

if (typeof exports !== 'undefined') {
  if (typeof module !== 'undefined' && module.exports) {
    exports = module.exports = _;  
  }
  exports._ = _ ;
} esle {
  root._ = _;
}

緩存局部變量及快速引用

underscore自己用到了很多ES5的原生方法,在瀏覽器支持的條件下,underscore率先使用原生的ES5方法。以下代碼所示,underscore經過局部變量來保存一些經常使用到的方法或者屬性。
這樣作有幾個好處:

  • 便於壓縮代碼
  • 提升代碼性能,減小在原型鏈中的查找次數
  • 同時也可減小代碼量,避免在使用時冗長的書寫
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;

undefined處理

在underscore中,有不少函數都會有一個context函數,也就是當前函數的執行上下文,underscore對其進行了處理,若是沒有傳入contextcontextundefined,則返回原函數。
這裏判斷值爲undefined用的是void 0,以下:

if (context === void 0) return func

做爲一隻涉獵尚淺的小白,查閱資料以後終於知道這裏做者爲何要用void 0來作判斷了。

詳情可點連接瞭解,這樣作更加安全可靠。
在還沒看到這個代碼時, 若是我要判斷一個值是否是undefined,我會這樣寫

if (context === undefined) {}

可是,在發現做者的void 0以後,才發現這樣寫並不可靠,在JavaScript中,咱們能夠這樣寫:

args => {
  let undefined = 1
  console.log(undefined) // => 1
  if (args === undefined) {
    //...
  }
}

若是這樣寫,undefined就被輕易地修改成了1,因此對於咱們以後定義的undefined的理解有歧義。因此,在JavaScript中,把undefined直接解釋爲「未定義」是有風險的,由於它可能被修改。

學習:之後判斷undefined直接使用void 0, 看起來也優雅一點(滑稽臉)。

處理類數組

// getLength 函數
// 該函數傳入一個參數,返回參數的 length 屬性值
// 用來獲取 array 以及 arrayLike 元素的 length 屬性值
var getLength = property('length');

// 判斷是不是 ArrayLike Object
// 類數組,即擁有 length 屬性而且 length 屬性值爲 Number 類型的元素
// 包括數組、arguments、HTML Collection 以及 NodeList 等等
// 包括相似 {length: 10} 這樣的對象
// 包括字符串、函數等
var isArrayLike = function(collection) {
  var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

對象建立的特殊處理

爲了處理Object.create的跨瀏覽器的兼容性,underscore進行了特殊的處理。咱們知道,原型是沒法直接實例化的,所以咱們先建立一個空對象,而後將其原型指向這個咱們想要實例化的原型,最後返回該對象其一個實例。其代碼以下:

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;  // 恢復Ctor的原型供下次使用
  return result;  // 返回該實例
};

underscore中的迭代(iteratee)

在函數式編程中,使用更多的是迭代,而不是循環。
迭代:

var res = _.map([1,2], function(item){
  return item * 2
})

循環:

var arr = [1,2]
var res = []
for(var i = 0; i < arr.length; i++) {
  res.push(arr[i] * 2)
}

在underscore中迭代使用很是巧妙,源碼也寫的很是好,經過傳入的數據類型不一樣而選擇不一樣的迭代函數。
首先,在underscore中_.map的實現以下:

_.map = _.collect = function(obj, iteratee, context) {
  iteratee = cb(iteratee, context);
  var keys = !isArrayLike(obj) && _.keys(obj),
      length = (keys || obj).length,
      results = Array(length);
  for (var index = 0; index < length; index++) {
    var currentKey = keys ? keys[index] : index;
    results[index] = iteratee(obj[currentKey], currentKey, obj) //(value, index, obj)
  }
  return results;
}

能夠看到,在_.map函數中的第二個參數iteratee,這個參數的格式能夠是函數,對象,字符串。underscore會將其處理成一個函數,這將由回調函數cb來完成,咱們來看一下cb的實現:

var cb = function(value, context, argCount) {
 // 是否用默認的迭代器 若是沒有傳入value 則返回當前迭代元素自身
 if (value == null) return _.identity;
 // 若是value是一個回調函數, 則須要優化回調 優化函數爲optimizeCb
 if (_.isFunction(value)) return optimizeCb(value, context, argCount);
 // 若是value是個對象, 則返回一個matcher進行對象匹配
 if (_.isObject(value)) return _.matcher(value)
 // 不然, 若是value只是一個字面量, 則把value看作是屬性名稱, 返回一個對應的屬性得到函數
 return _.property(value);
}

前面兩個比較容易理解,看看當傳入的數據格式爲對象的狀況,若是 value 傳入的是一個對象,那麼返回iteratee(_.matcher)的目的是想要知道當前被迭代元素是否匹配給定的這個對象:

var results = _.map([{name:'water'},{name: 'lzb',age:13}], {name: 'lzb'});
// => results: [false,true]

若是傳入的是字面量,如數字,字符串等, 他會返回對應的key值,以下:

var results = _.map([{name:'water'},{name:'lzb'}],'name');
// => results: ['water', 'lzb'];

回調處理

在上面的cb函數中,咱們能夠看到,當傳入的數據格式是函數,則須要經過optimizeCb函數進行統一處理,返回對應的回調函數,下面是underscore中optimizeCb函數的實現:

// 回調處理
// underscore 內部方法
// 根據 this 指向(context 參數)
// 以及 argCount 參數
// 二次操做返回一些回調、迭代方法
var optimizeCb = function(func, context, argCount) {
  // // void 0 會返回純正的undefined,這樣作避免undefined已經被污染帶來的斷定失效
  if (context === void 0) return func;
  switch (argCount == null ? 3 : argCount) {
    // 回調參數爲1時, 即迭代過程當中,咱們只須要值
    // _.times
    case 1: return function(value) {
      return func.call(context, value);
    };
    case 2: return function(value, other) {
      return func.call(context, value, other);
    };
    // 3個參數(值,索引,被迭代集合對象)
    // _.each、_.map  (value, key, obj)
    case 3: return function(value, index, collection) {
      return func.call(context, value, index, collection);
    };
    // 4個參數(累加器(好比reducer須要的), 值, 索引, 被迭代集合對象)
    // _.reduce、_.reduceRight
    case 4: return function(accumulator, value, index, collection) {
      return func.call(context, accumulator, value, index, collection);
    };
  }

  // 若是都不符合上述的任一條件,直接使用apply調用相關函數
  return function() {
    return func.apply(context, arguments);
  };
}

optimizeCb 的整體思路就是:傳入待優化的回調函數 func,以及迭代回調須要的參數個數argCount,根據參數個數分狀況進行優化。

在underscore的_.times函數視線中,_times的做用執行一個傳入iteratee函數n次,並返回由每次執行結果組成的數組。它的迭代過程iteratee只須要1個參數(當前迭代的索引)
_.times函數在underscore中的實現:

_.times = function(n, iteratee, context) {
  vat accum = Array(Math.max(0, n));
  iteratee = optimizeCb(iteratee, context, 1);
  for (var i = 0; i < n; i++) accum[i] = iteratee(i);
  return accum;
}

_.times的使用

function getIndex(index) {
  return index;
}
var results = _.times(3, getIndex); // => [0,1,2]

optimizeCb函數中當argCount的個數爲2的狀況並不常見,在_.each,_.map等函數中,argCount的值爲3(value, key, obj),當argCount須要四個參數時,這四個參數的格式爲:

  • accumulator:累加器
  • value:迭代元素
  • index:迭代索引
  • collection:當前迭代集合

underscore中reduce的實現以下:

/**
 * reduce函數的工廠函數, 用於生成一個reducer, 經過參數決定reduce的方向
 * @param dir 方向 left or right
 * @returns {function}
 */
function createReduce(dir) {
  function iterator(obj, iteratee, memo, keys, index, length) {
    for(; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      // memo 用來記錄最新的 reduce 結果
      // 執行 reduce 回調, 刷新當前值
      memo = iteratee(memo, obj[currentKey], currentKey, obj);
    }
    return memo;
  }
  return function(obj, iteratee, memo, context) {
    // 優化回調
    iteratee = optimizeCb(iteratee, context, 4);
    var keys = !isArrayLike(obj) && _.keys(obj),
        length = (keys || obj).length,
        index = dir > 0 ? 0 : length - 1;
    if (arguments.length < 3) {
      // 若是沒有傳入memo初始值 則從左第一個爲初始值 從右則最後一個爲初始值
      memo = obj[keys ? keys[index] : index];
      index += dir;
    }
    // return func
    return iterator(obj, iteratee, memo, keys, index, length);
  }
}

例如在_.reduce、_.reduceRight中,argCount的值爲4。看看underscore中_.reduce的使用例子

var sum = _.reduce([1,2,3,4], function(accumulator, value, index, collection){
  return accumulator + value;
}, 0) // => 10
相關文章
相關標籤/搜索