Underscore 源碼(二)經常使用思路和類型判斷

前面已經介紹過了,關於 _ 在內部是一個什麼樣的狀況,其實就是定義了一個名字叫作 _ 的函數,函數自己就是對象呀,就在 _ 上擴展了 100 多種方法。javascript

起個頭

接着上一篇文章的內容往下講,第一個擴展的函數是 each 函數,這和數組的 forEach 函數很像,即便不是數組,是僞數組,也能夠經過 call 的方式來解決循環遍歷,forEach 接受三個參數,且沒有返回值,不對原數組產生改變。來看看 each 函數:php

_.each = _.forEach = function(obj, iteratee, context) {
  iteratee = optimizeCb(iteratee, context);
  var i, length;
  if (isArrayLike(obj)) {
    for (i = 0, length = obj.length; i < length; i++) {
      iteratee(obj[i], i, obj);
    }
  } else {
    var keys = _.keys(obj);
    for (i = 0, length = keys.length; i < length; i++) {
      iteratee(obj[keys[i]], keys[i], obj);
    }
  }
  return obj;
};

each 函數接收三個參數,分別是 obj 執行體,回調函數和回調函數的上下文,回調函數會經過 optimizeCb 來優化,optimizeCb 沒有傳入第三個參數 argCount,代表默認是三個,可是若是上下文 context 爲空的狀況下,就直接返回 iteratee 函數。css

isArrayLike 前面已經介紹過了,不一樣於數組的 forEach 方法,_ 的 each 方法能夠處理對象,只不過要先調用 _.keys 方法獲取對象的 keys 集合。返回值也算是一個特色吧,each 函數返回 obj,而數組的方法,是沒有返回值的。html

第二個是 map 函數:java

_.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);
  }
  return results;
};

套路都是同樣的,既能夠處理數組,又能夠處理對象,可是 map 函數要有一個返回值,不管是數組,仍是對象,返回值是一個數組,並且從代碼能夠看到,新生成了數組,不會對原數組產生影響。node

而後就是 reduce 函數,感受介紹完這三個就能夠召喚神龍了,其中 reduce 分爲左和右,以下:git

_.reduce = _.foldl = _.inject = createReduce(1);

_.reduceRight = _.foldr = createReduce(-1);

爲了減小代碼量,就用 createReduce 函數,接收 1 和 -1 參數:github

function createReduce(dir) {
  // iterator 函數是執行,在最終結果裏面
  function iterator(obj, iteratee, memo, keys, index, length) {
    for (; index >= 0 && index < length; index += dir) {
      var currentKey = keys ? keys[index] : index;
      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 = obj[keys ? keys[index] : index];
      index += dir;
    }
    return iterator(obj, iteratee, memo, keys, index, length);
  };
}

createReduce 用閉包返回了一個函數,該函數接受四個參數,分別是執行數組或對象、回調函數、初始值和上下文,我的感受這裏的邏輯有點換混亂,好比我只有三個參數,有初始值沒有上下文,這個好辦,可是若是一樣是三個參數,我是有上下文,可是沒有初始值,就會致使流程出現問題。不過我也沒有比較好的解決辦法。面試

當參數爲兩個的時候,初始值沒有,就會調用數組或對象的第一個參數做爲初始值,並把指針後移一位(這裏用指針,實際上是數組的索引),傳入的函數,它有四個參數,這和數組 reduce 方法是同樣的。express

我的認爲,reduce 用來處理對象,仍是有點問題的,好比獲取對象的 keys 值,若是每次獲取的順序都不同,致使處理的順序也不同,那最終的結果還會同樣嗎?因此我決定處理對象仍是要謹慎點好。

_.findKey = function(obj, predicate, context) {
  predicate = cb(predicate, context);
  var keys = _.keys(obj), key;
  for (var i = 0, length = keys.length; i < length; i++) {
    key = keys[i];
    if (predicate(obj[key], key, obj)) return key;
  }
};
_.find = _.detect = function(obj, predicate, context) {
  var key;
  if (isArrayLike(obj)) {
    key = _.findIndex(obj, predicate, context);
  } else {
    key = _.findKey(obj, predicate, context);
  }
  if (key !== void 0 && key !== -1) return obj[key];
};

find 也是一個已經在數組方法中實現的,對應於數組的 findfindIndex 函數。在 _中,_.findKey 針對於對象,_.findIndex 針對於數組,又略有不一樣,可是討論和 reduce 的套路是同樣的:

function createPredicateIndexFinder(dir) {
  return function(array, predicate, context) {
    predicate = cb(predicate, context);
    var length = getLength(array);
    var index = dir > 0 ? 0 : length - 1;
    for (; index >= 0 && index < length; index += dir) {
      if (predicate(array[index], index, array)) return index;
    }
    return -1;
  };
}

_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);

有重點的來看

後面以爲有的函數真的是太無聊了,套路都是一致的,仔細看了也學不到太多的東西,感受仍是有選擇的來聊聊吧。underscore-analysis,這篇博客裏的內容寫得挺不錯的,不少內容都一針見血,準備按照博客中的思路來解讀源碼,不打算一步一步來了,太無聊。

類型判斷

jQuery 裏面有一個判斷類型的函數,就是 $.type,它最主要的好處就是一個函數能夠對因此的類型進行判斷,而後返回類型名。_ 中的判斷略坑,函數不少,並且都是以 is 開頭,什麼 isArrayisFunction等等。

var toString = Object.prototype.toString,
  nativeIsArray = Array.isArray;

_.isArray = nativeIsArray || function(obj) {
  return toString.call(obj) === '[object Array]';
};

能夠看得出來,設計者的心思仍是挺仔細的,固然,還有:

_.isObject = function(obj) {
  var type = typeof obj;
  return type === 'function' || type === 'object' && !!obj;
};
_.isBoolean = function(obj) {
  return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
};

isObject 的流程看起來有點和 array、boolean 不同,可是也是情理之中,很好理解,那麼問題來了,這樣會不會很麻煩,光構造這些函數就要花好久的時間吧,答案用下面的代碼來解釋:

_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
  _['is' + name] = function(obj) {
    return toString.call(obj) === '[object ' + name + ']';
  };
});

對於一些不用特殊處理的函數,直接用 each 函數來搞定。

除此以外,還有一些有意思的 is 函數:

// 只能用來判斷 NaN 類型,由於只有 NaN !== NaN 成立,其餘 Number 均不成立
_.isNaN = function(obj) {
  return _.isNumber(obj) && obj !== +obj;
};

// null 嚴格等於哪些類型?
_.isNull = function(obj) {
  return obj === null;
};

// 又是一個嚴格判斷 ===
// 貌似 _.isUndefined() == true 空參數的狀況也是成立的
_.isUndefined = function(obj) {
  return obj === void 0;
};

不過對於 isNaN 函數,仍是有 bug 的,好比:

_.isNaN(new Number(1)); // true
// new Number(1) 和 Number(1) 是有區別的

這邊 github issue 上已經有人提出了這個問題,_.isNaN,也合併到分支了 Fixes _.isNaN for wrapped numbers,可是不知道爲何我這個 1.8.3 版本仍是老樣子,難度我下載了一個假的 underscore?issue 中提供瞭解決辦法:

_.isNaN = function(obj) {
  // 將 !== 換成 !=
  return _.isNumber(obj) && obj != +obj;
};

我跑去最新發布的 underscore 下面看了下,最近更新 4 month ago,搜索了一下 _.isNaN

_.isNaN = function(obj) {
  // 真的很機智,NaN 是 Number 且 isNaN(NaN) == true
  // new Number(1) 此次返回的是 false 了
  return _.isNumber(obj) && isNaN(obj);
};

來看一眼 jQuery 裏面的類型判斷:

// v3.1.1
var class2type = {
    "[object Boolean]": "boolean",
    "[object Number]": "number",
    "[object String]": "string",
    "[object Function]": "function",
    "[object Array]": "array",
    "[object Date]": "date",
    "[object RegExp]": "regexp",
    "[object Object]": "object",
    "[object Error]": "error",
    "[object Symbol]": "symbol"
}
var toString = Object.prototype.toString;

jQuery.type = function (obj) {
    if (obj == null) {
        return obj + "";
    }
    return 
      typeof obj === "object" || typeof obj === "function" ? 
        class2type[toString.call(obj)] || "object" : 
        typeof obj;
}

比較了一下,發現 jQuery 相比於 underscore,少了 Arguments 的判斷,多了 ES6 的 Symbol 的判斷(PS:underscore 很久沒人維護了?)。因此 jQuery 對 Arguments 的判斷只能返回 object_ 中是沒有 _.isSymbol 函數的。之前一直看 jQuery 的類型判斷,居然不知道 Arguments 也能夠單獨分爲一類 arguments。還有就是,若是讓我來選擇在項目中使用哪一個,我確定選擇 jQuery 的這種方式,儘管 underscore 更詳細,可是函數拆分太多了。

其餘有意思的 is 函數

前面說了,underscore 給人一種很囉嗦的感受,is 函數太多,話雖如此,總有幾個很是有意思的函數:

_.isEmpty = function(obj) {
  if (obj == null) return true;
  if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
  return _.keys(obj).length === 0;
};

isEmpty 用來判斷是否爲空,我剛開始看到這個函數的時候,有點懵,說到底仍是對 Empty 這個詞理解的不夠深入。到底什麼是 呢,看源碼,我以爲這是最好的答案,畢竟聚集了那麼多優秀多 JS 開發者。

  1. 全部與 null 相等的元素,都爲空,沒問題;

  2. 數組、字符串、Arguments, 它們也能夠爲空,好比 length 屬性爲 0 的時候;

  3. 最後,用自帶的 _.keys 判斷 obj key 集合的長度是否爲 0。

有時候以爲看代碼,真的是一種昇華。

還有一個 isElement,很簡單,只是不明白爲何用了兩次非來判斷:

_.isElement = function(obj) {
  // !! 
  return !!(obj && obj.nodeType === 1);
};

重點來講下 isEqual 函數:

_.isEqual = function(a, b) {
  return eq(a, b);
};

var eq = function(a, b, aStack, bStack) {
  // 解決 0 和 -0 不該該相等的問題?
  // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
  if (a === b) return a !== 0 || 1 / a === 1 / b;
  // 有一個爲空,直接返回,但要注意 undefined !== null
  if (a == null || b == null) return a === b;
  // 若是 a、b 是 _ 對象,返回它們的 warpped
  if (a instanceof _) a = a._wrapped;
  if (b instanceof _) b = b._wrapped;
  var className = toString.call(a);
  // 類型不一樣,直接返回 false
  if (className !== toString.call(b)) return false;
  switch (className) {
    // Strings, numbers, regular expressions, dates, and booleans are compared by value.
    case '[object RegExp]':
    // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
    case '[object String]':
      // 經過 '' + a 構造字符串
      return '' + a === '' + b;
    case '[object Number]':
      // +a 能夠將類型爲 new Number 的 a 轉變爲數字
      // +a !== +a,只能說明 a 爲 NaN,判斷 b 是否也爲 NaN
      if (+a !== +a) return +b !== +b;
      return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case '[object Date]':
    case '[object Boolean]':
      // +true === 1
      // +false === 0
      return +a === +b;
  }

  // 若是以上都不能知足,可能判斷的類型爲數組或對象,=== 是沒法解決的
  var areArrays = className === '[object Array]';
  // 非數組的狀況,看一下它們是否同祖先,不一樣祖先,failed
  if (!areArrays) {
    // 奇怪的數據
    if (typeof a != 'object' || typeof b != 'object') return false;

    // Objects with different constructors are not equivalent, but `Object`s or `Array`s
    // from different frames are.
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
                             _.isFunction(bCtor) && bCtor instanceof bCtor)
                        && ('constructor' in a && 'constructor' in b)) {
      return false;
    }
  }
  // Assume equality for cyclic structures. The algorithm for detecting cyclic
  // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

  // Initializing stack of traversed objects.
  // It's done here since we only need them for objects and arrays comparison.
  aStack = aStack || [];
  bStack = bStack || [];
  var length = aStack.length;
  while (length--) {
    // 這個應該是爲了防止嵌套循環
    if (aStack[length] === a) return bStack[length] === b;
  }

  // Add the first object to the stack of traversed objects.
  aStack.push(a);
  bStack.push(b);

  // Recursively compare objects and arrays.
  if (areArrays) {
    length = a.length;
    // 數組長度都不等,確定不同
    if (length !== b.length) return false;
    // 遞歸比較,若是有一個不一樣,返回 false
    while (length--) {
      if (!eq(a[length], b[length], aStack, bStack)) return false;
    }
  } else {
    // 非數組的狀況
    var keys = _.keys(a), key;
    length = keys.length;
    // 兩個對象的長度不等
    if (_.keys(b).length !== length) return false;
    while (length--) {
      key = keys[length];
      if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
    }
  }
  // 清空數組
  aStack.pop();
  bStack.pop();
  return true; // 一路到底,沒有失敗,則返回成功
};

總結一下,就是 _.isEqual 內部雖然用的是 === 這種判斷,可是對於 === 判斷失敗的狀況,isEqual 會嘗試將比較的元素拆分比較,好比,若是是兩個不一樣引用地址數組,它們元素都是同樣的,則返回 true

[22, 33] === [22, 33]; // false
_.isEqual([22, 33], [22, 33]); // true

{id: 3} === {id: 3}; // false
_.isEqual({id: 3}, {id: 3}); // true

NaN === NaN; // false
_.isEqual(NaN, NaN); // true

/a/ === new RegExp('a'); // false
_.isEqual(/a/, new RegExp('a')); // true

能夠看得出來,isEqual 是一個很是有心機的函數。

數組去重

關於數組去重,從面試筆試的程度來講,是屢見不鮮的題目,實際中也會常常用到,前段時間看到一篇去重的博客,感受含金量很高,地址在這:也談JavaScript數組去重,年代在久一點,就是玉伯大蝦的從 JavaScript 數組去重談性能優化

_ 中也有去重函數 uniq 或者 unique:

_.uniq = _.unique = function(array, isSorted, iteratee, context) {
  // 和 jQuery 同樣,平移參數
  if (!_.isBoolean(isSorted)) {
    context = iteratee;
    iteratee = isSorted;
    isSorted = false;
  }
  // 又是回調 cb,三個參數
  if (iteratee != null) iteratee = cb(iteratee, context);
  var result = [];
  var seen = [];
  for (var i = 0, length = getLength(array); i < length; i++) {
    var value = array[i],
        computed = iteratee ? iteratee(value, i, array) : value;
    // 若是已經排列好,就直接和前一個進行比較
    if (isSorted) {
      if (!i || seen !== computed) result.push(value);
      seen = computed;
    } else if (iteratee) {
      // seen 此時化身爲一個去重數組,前提是有 iteratee 函數
      if (!_.contains(seen, computed)) {
        seen.push(computed);
        result.push(value);
      }
    } else if (!_.contains(result, value)) {
      result.push(value);
    }
  }
  return result;
};

仍是要從 unique 的幾個參數提及,第一個參數是數組,第二個表示是否已經排好序,第三個參數是一個函數,表示對數組的元素進行怎樣的處理,第四個參數是第三個參數的上下文。返回值是一個新數組,思路也很清楚,對於已經排好序的數組,用後一個和前一個相比,不同就 push 到 result 中,對於沒有排好序的數組,要用到 _.contains 函數對 result 是否包含元素進行判斷。

去重的話,若是數組是排好序的,效率會很高,時間複雜度爲 n,只要遍歷一次循環即刻,對於未排好序的數組,要頻繁的使用 contains 函數,複雜度很高,平均爲 n 的平方。去重所用到爲相等爲嚴格等於 ===,使用的時候要當心。

_.contains 函數以下所示:

_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
  if (!isArrayLike(obj)) obj = _.values(obj);
  if (typeof fromIndex != 'number' || guard) fromIndex = 0;
  return _.indexOf(obj, item, fromIndex) >= 0;
};

_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);

function createIndexFinder(dir, predicateFind, sortedIndex) {
  return function(array, item, idx) {
    var i = 0, length = getLength(array);
    if (typeof idx == 'number') {
      if (dir > 0) {
          i = idx >= 0 ? idx : Math.max(idx + length, i);
      } else {
          length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
      }
    } else if (sortedIndex && idx && length) {
      idx = sortedIndex(array, item);
      return array[idx] === item ? idx : -1;
    }
    // 本身都不等於本身,讓我想到了 NaN
    if (item !== item) {
      idx = predicateFind(slice.call(array, i, length), _.isNaN);
      return idx >= 0 ? idx + i : -1;
    }
    for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
      // 這裏使用的是嚴格等於
      if (array[idx] === item) return idx; // 找到,返回索引
    }
    return -1; // 沒找到,返回 -1
  };
}

總結

感受 Underscore 的源碼看起來仍是很簡單的,Underscore 裏面有一些過期的函數,這些均可以拿過來學習,邏輯比較清晰,並不像 jQuery 那樣,一個函數裏面好多內部函數,看着看着就暈了。

參考

Underscore.js (1.8.3) 中文文檔
經常使用類型判斷以及一些有用的工具方法
也談JavaScript數組去重
JavaScript 數組去重

歡迎來個人博客交流。

相關文章
相關標籤/搜索