Underscore.js 1.8.3 學習筆記

underscore.js源碼加註釋一共1500多行,它提供了一整套函數式編程實用的功能,一共一百多個函數,幾乎每個函數均可以做爲參考典範。初讀的時候,真是一臉懵圈,各類函數閉包、迭代和嵌套的使用,讓我一時很難消化。
在這裏,我來記錄一下我學習underscore.js的一些發現,以及幾個我認爲比較經典的函數使用。css

首先咱們能夠看到,underscore.js中全部的函數和方法都在一個閉包裏:(function() {...}.call(this));這麼作的目的是爲了不污染全局變量。正則表達式

爲了壓縮代碼,underscore中用到將原型賦值給變量保存的方法:算法

// Save bytes in the minified (but not gzipped) version:
  // 原型賦值,便於壓縮代碼,這裏的壓縮指壓縮到min.js而不是gzip壓縮
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  // Create quick reference variables for speed access to core prototypes.
  // 將內置對象原型中的經常使用方法賦值給引用變量,減小在原型鏈中的查找次數,從而提升代碼效率
  var
    push             = ArrayProto.push,
    slice            = ArrayProto.slice,
    toString         = ObjProto.toString,
    hasOwnProperty   = ObjProto.hasOwnProperty;

咱們在處理代碼時,不能直接將Array.prototype等直接壓縮,由於壓縮事後,瀏覽器是沒法識別這些壓縮字段的。壓縮事後,咱們在使用obj.prototype方法時,直接使用其相對應的變量就能夠了。若是在咱們的代碼中會屢次用到某個方法,用上面的方法就進行處理,使用起來就方便多了。express

接着建立了一個"_"對象,以後將underscore中的相關方法添加到"_"原型中,那麼建立的"_"對象也就具有了underscore方法。編程

// 建立一個"_"對象
var _ = function(obj) {
    if (obj instanceof _) return obj;  //若是obj是"—"的實例,則直接返回obj
    if (!(this instanceof _)) return new _(obj);  //若是不是,則調用new運算符,返回實例化的對象
    this._wrapped = obj;    //將underscore對象存放在_.wrapped屬性中
  };

上面用到了instanceof運算符,JavaScript中instanceof運算符是返回一個 Boolean 值,指出對象是不是特定類的一個實例。
使用方法:result = object instanceof class
其中,result是必選項,表任意變量;object是必選項,表任意對象表達式;class是必選項,表任意已定義的對象類。若是 object 是 class 的一個實例,則 instanceof 運算符返回 true。若是 object 不是指定類的一個實例,或者 object 是 null,則返回 false。api

接下來我就列舉幾個underscore中的函數。數組

一、_.each瀏覽器

_.each = _.forEach = function(obj, iteratee, context) {
    iteratee = optimizeCb(iteratee, context);  // 根據 context 肯定不一樣的迭代函數
    var i, length;
    if (isArrayLike(obj)) { // 若是是類數組 (默認不會傳入相似 {length: 10} 這樣的數據)
      for (i = 0, length = obj.length; i < length; i++) {  //遍歷
        iteratee(obj[i], i, obj);
      }
    } else {  // 若是 obj 是對象
      var keys = _.keys(obj);  // 獲取對象的全部 key 值
      for (i = 0, length = keys.length; i < length; i++) {  //若是是對象,則遍歷處理 values 值
        iteratee(obj[keys[i]], keys[i], obj);
      }
    }
    return obj; //返回 obj 參數,供鏈式調用(Returns the list for chaining)
  };

_.each = _.forEach = function(obj, iteratee, context)中,一共有三個參數:
第一個參數爲數組(包括類數組)或者對象;第二個參數爲迭代方法,對數組或者對象每一個元素都執行
該方法,該方法又能傳入三個參數,分別爲 (item, index, array)((value, key, obj) for object);
第三個參數(可省略)肯定第二個參數 iteratee 函數中的(可能有的)this 指向,
即 iteratee 中出現的(若是有)全部 this 都指向 context。

二、_.contains緩存

_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
    if (!isArrayLike(obj)) obj = _.values(obj); // // 若是是對象,返回 values 組成的數組
    //fromIndex 表示查詢起始位置,若是沒有指定該參數,則默認從頭找起
    if (typeof fromIndex != 'number' || guard) fromIndex = 0;
    //_.indexOf 是數組的擴展方法(Array Functions)
    //數組中尋找某一元素
    return _.indexOf(obj, item, fromIndex) >= 0; 
  };

判斷數組或者對象中(value 值)是否有指定元素,若是是 object,則忽略 key 值,只須要查找 value 值便可,若是obj 中是否有指定的 value 值,則返回布爾值。閉包

三、 _.uniq

_.uniq = _.unique = function(array, isSorted, iteratee, context) {
    if (!_.isBoolean(isSorted)) { // 沒有傳入 isSorted 參數
      context = iteratee;
      iteratee = isSorted;
      isSorted = false;     // 轉爲 _.unique(array, false, undefined, iteratee)
    }
    // 若是有迭代函數,則根據 this 指向二次返回新的迭代函數
    if (iteratee != null) iteratee = cb(iteratee, context); 
    var result = [];  // 結果數組,是 array 的子集
    var seen = [];    //// 已經出現過的元素(或者通過迭代過的值),用來過濾重複值
    for (var i = 0, length = getLength(array); i < length; i++) {
      var value = array[i],
      //若是指定了迭代函數,則對數組每個元素進行迭代
      //迭代函數傳入的三個參數一般是 value, index, array 形式
          computed = iteratee ? iteratee(value, i, array) : value; 
          //若是是有序數組,則當前元素只需跟上一個元素對比便可,並用 seen 變量保存上一個元素
      if (isSorted) {
          //若是 i === 0,是第一個元素,則直接 push,不然比較當前元素是否和前一個元素相等
        if (!i || seen !== computed) result.push(value);
        seen = computed; // seen 保存當前元素,供下一次對比
      } else if (iteratee) {
        if (!_.contains(seen, computed)) { //// 若是 seen[] 中沒有 computed 這個元素值
          seen.push(computed);
          result.push(value);
        }
      } else if (!_.contains(result, value)) {
          // 若是不用通過迭代函數計算,也就不用 seen[] 變量了
        result.push(value);
      }
    }
    return result;
  };

這是一個數組去重函數,若是函數參數中第二個參數 isSorted 爲 true,則說明事先已經知道數組有序,程序會跑一個更快的算法;若是有第三個參數 iteratee,則對數組每一個元素迭代,對迭代以後的結果進行去重,而後返回去重後的數組(array 的子數組);另外,暴露的 API 中沒 context 參數。

四、_.range

// 返回某一個範圍內的數組成的數組
  _.range = function(start, stop, step) {
    if (stop == null) {
      stop = start || 0;
      start = 0;
    }
    step = step || 1;
    // 返回數組的長度
    var length = Math.max(Math.ceil((stop - start) / step), 0);
    var range = Array(length);  // 返回的數組

    for (var idx = 0; idx < length; idx++, start += step) {
      range[idx] = start;
    }

    return range;
  };

_.range([start], stop, [step]) 是一個用來建立整數靈活編號的列表的函數,便於each 和 map循環。若是省略start則默認爲 0;step 默認爲 1.返回一個從start 到stop的整數的列表,用step來增長 (或減小)獨佔。值得注意的是,若是stop值在start前面(也就是stop值小於start值),那麼值域會被認爲是零長度,而不是負增加。-若是要一個負數的值域 ,則使用負數step. (參考http://www.css88.com/doc/unde...

五、_.bind

_.bind = function(func, context) {
    if (nativeBind && func.bind === nativeBind) 
        // 若是瀏覽器支持 ES5 bind 方法,而且 func 上的 bind 方法沒有被重寫,則優先使用原生的 bind 方法
        return nativeBind.apply(func, slice.call(arguments, 1));

    if (!_.isFunction(func)) 
        //若是傳入的參數 func 不是方法,則拋出錯誤
        throw new TypeError('Bind must be called on a function');

    //經典閉包,函數返回函數
    var args = slice.call(arguments, 2); // args 獲取優先使用的參數
    var bound = function() {
        //最終函數的實際調用參數由兩部分組成
        //一部分是傳入 _.bind 的參數(會被優先調用),另外一部分是傳入 bound(_.bind 所返回方法)的參數
      return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
    };
    return bound;
  };

該方法是ES5 bind 方法的擴展, 將 func 中的 this 指向 context(對象),用法爲_.bind(function, object, *arguments),其中arguments 參數可選,它會被看成 func 的參數傳入,func 在調用時,會優先用 arguments 參數,而後使用 _.bind 返回方法所傳入的參數。

六、 _.memoize

_.memoize = function(func, hasher) {
    var memoize = function(key) { // 儲存變量,方便使用
      var cache = memoize.cache;
      //求 key
      //若是傳入了 hasher,則用 hasher 函數來計算 key,不然用 參數 key(即 memoize 方法傳入的第一個參數)當 key
      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
      //若是這個 key 還沒被 hash 過(還沒求過值)
      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
      return cache[address];
    };
    memoize.cache = {}; // cache 對象被當作 key-value 鍵值對緩存中間運算結果
    return memoize; // 返回一個函數(經典閉包)
  };

Memoizes方法能夠緩存某函數的計算結果,用法:_.memoize(function, [hashFunction])
若是傳遞了 hashFunction 參數,就用 hashFunction 的返回值做爲key存儲函數的計算結果。hashFunction 默認使用function的第一個參數做爲key。memoized值的緩存可做爲返回函數的cache屬性。

七、_.throttle

_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;  //標記時間戳,上一次執行回調的時間戳
    var previous = 0;
    if (!options)  //若是沒有傳入 options 參數
        options = {};  // 則將 options 參數置爲空對象
    var later = function() {
    //若是 options.leading === false,則每次觸發回調後將 previous 置爲 0,不然置爲當前時間戳
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    // _.throttle 方法返回的函數
    return function() {
      var now = _.now();  // 記錄當前時間戳
      //第一次執行回調(此時 previous 爲 0,以後 previous 值爲上一次時間戳)
      //而且若是程序設定第一個回調不是當即執行的(options.leading === false),則將 previous 值(表示上次執行的時間戳)設爲 now 的時間戳(第一次觸發時),表示剛執行過,此次就不用執行了
      if (!previous && options.leading === false) previous = now;

       // 距離下次觸發 func 還須要等待的時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout); // 解除引用,防止內存泄露
          timeout = null;
        }
        // 重置前一次觸發的時間戳
        previous = now;
        // 觸發方法,result 爲該方法返回值
        result = func.apply(context, args);

        if (!timeout) 
            context = args = null; //引用置爲空,防止內存泄露
      } else if (!timeout && options.trailing !== false) {// 最後一次須要觸發的狀況
        //若是已經存在一個定時器,則不會進入該 if 分支
        // 若是 {trailing: false},即最後一次不須要觸發了,也不會進入這個分支
        timeout = setTimeout(later, remaining); // 間隔 remaining milliseconds 後觸發 later 方法
      }
      return result;  // 回調返回值
    };
  };

函數節流(若是有連續事件響應,則每間隔必定時間段觸發),每間隔 wait(Number) milliseconds 觸發一次 func 方法,若是 options 參數傳入 {leading: false},不會立刻觸發(等待 wait milliseconds 後第一次觸發 func),若是 options 參數傳入 {trailing: false},那麼最後一次回調不會被觸發。options 不能同時設置 leading 和 trailing 爲 false。

八、_.debounce

_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
        //定時器設置的回調 later 方法的觸發時間,和連續事件觸發的最後一次時間戳的間隔
      var last = _.now() - timestamp;  //若是間隔爲 wait(或者恰好大於 wait),則觸發事件

      if (last < wait && last >= 0) { //時間間隔 last 在 [0, wait) 中,還沒到觸發的點,則繼續設置定時器
        timeout = setTimeout(later, wait - last);
      } else {  //到了能夠觸發的時間點
        timeout = null;
         // 若是不是當即執行,隨即執行 func 方法
        if (!immediate) {
          result = func.apply(context, args); // 執行 func 函數
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      //每次觸發函數,更新時間戳
      timestamp = _.now(); 
      // 當即觸發須要知足兩個條件
      // immediate 參數爲 true,而且 timeout 還沒設置
      var callNow = immediate && !timeout; // 設置 wait seconds 後觸發 later 方法
      // 在某一段的連續觸發中,只會在第一次觸發時進入這個 if 分支中
      if (!timeout) // 設置了 timeout,因此之後不會進入這個 if 分支了
          timeout = setTimeout(later, wait);
      // 若是是當即觸發
      if (callNow) {
        result = func.apply(context, args);
        context = args = null; // 解除引用
      }

      return result;
    };
  };

函數去抖(連續事件觸發結束後只觸發一次),如_.debounce(function(){}, 1000)表示連續事件結束後的 1000ms 後觸發。

九、 _.pick

_.pick = function(object, oiteratee, context) {
    var result = {}, // result 爲返回的對象副本
    obj = object, iteratee, keys;
    if (obj == null) return result;
    // 若是第二個參數是函數
    if (_.isFunction(oiteratee)) {
      keys = _.allKeys(obj);
      iteratee = optimizeCb(oiteratee, context);
    } else {
    // 若是第二個參數不是函數
    // 則後面的 keys 多是數組
    // 也多是連續的幾個並列的參數
    // 用 flatten 將它們展開
      keys = flatten(arguments, false, false, 1);
      //也轉爲 predicate 函數判斷形式,將指定 key 轉化爲 predicate 函數
      iteratee = function(value, key, obj) { return key in obj; };
      obj = Object(obj);
    }
    for (var i = 0, length = keys.length; i < length; i++) {
      var key = keys[i];
      var value = obj[key];
      if (iteratee(value, key, obj)) result[key] = value;
    }
    return result;
  };

_.pick(object, *keys)根據必定的需求(key 值,或者經過 predicate 函數返回真假),返回擁有必定鍵值對的對象副本。第二個參數能夠是一個 predicate 函數,也能夠是0個或多個key。

十、_.noConflict

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

若是全局環境中已經使用了 _ 變量,能夠用該方法返回其餘變量,繼續使用 underscore 中的方法。

十一、_.template

_.template = function(text, settings, oldSettings) {
      // 兼容舊版本
    if (!settings && oldSettings) settings = oldSettings;
    settings = _.defaults({}, settings, _.templateSettings);

    // Combine delimiters into one regular expression via alternation.
    // // 正則表達式 pattern,用於正則匹配 text 字符串中的模板字符串
    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

    // Compile the template source, escaping string literals appropriately.
    // 編譯模板字符串,將原始的模板字符串替換成函數字符串
    // 用拼接成的函數字符串生成函數(new Function(...))
    var index = 0;
    // source 變量拼接的字符串用來生成函數,用於當作 new Function 生成函數時的函數字符串變量
    var source = "__p+='";
    // replace 函數不須要爲返回值賦值,主要是爲了在函數內對 source 變量賦值
    // // 將 text 變量中的模板提取出來
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
        // escape/interpolate/evaluate 爲匹配的子表達式(若是沒有匹配成功則爲 undefined)
       // offset 爲字符匹配(match)的起始位置(偏移量)
      source += text.slice(index, offset).replace(escaper, escapeChar);

      index = offset + match.length;   // 改變 index 值,爲了下次的 slice

      if (escape) {
          // 須要對變量進行編碼(=> HTML 實體編碼)
          // 避免 XSS 攻擊
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      } else if (interpolate) {  // 單純的插入變量
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) {
        source += "';\n" + evaluate + "\n__p+='";
      }

      // Adobe VMs need the match returned to produce the correct offest.
      // return 的做用是將匹配到的內容原樣返回(Adobe VMs 須要返回 match 來使得 offset 值正常)
      return match;
    });
    source += "';\n";

    // If a variable is not specified, place data values in local scope.
    // 若是設置了 settings.variable,能顯著提高模板的渲染速度,不然,默認用 with 語句指定做用域
    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';

    // 增長 print 功能
    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + 'return __p;\n';   // __p 爲返回的字符串

    try {
        // render 方法,前兩個參數爲 render 方法的參數
        // obj 爲傳入的 JSON 對象,傳入 _ 參數使得函數內部能用 Underscore 的函數
      var render = new Function(settings.variable || 'obj', '_', source);
    } catch (e) {
      e.source = source;
      throw e;
    }

    // 返回的函數
    //data 通常是 JSON 數據,用來渲染模板
    var template = function(data) {
        // render 爲模板渲染函數
      return render.call(this, data, _);  // 傳入參數 _ ,使得模板裏 <%  %> 裏的代碼能用 underscore 的方法
    };

    // Provide the compiled source as a convenience for precompilation.
    var argument = settings.variable || 'obj';
    template.source = 'function(' + argument + '){\n' + source + '}';

    return template;
  };
_.template(templateString, [settings])  // 將 JavaScript 模板編譯爲能夠用於頁面呈現的函數,setting 參數能夠用來自定義字符串模板,是一個哈希表包含任何能夠覆蓋的設置。
相關文章
相關標籤/搜索