前面已經介紹過了,關於 _
在內部是一個什麼樣的狀況,其實就是定義了一個名字叫作 _
的函數,函數自己就是對象呀,就在 _
上擴展了 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
也是一個已經在數組方法中實現的,對應於數組的 find
和 findIndex
函數。在 _
中,_.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
開頭,什麼 isArray
,isFunction
等等。
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 更詳細,可是函數拆分太多了。
前面說了,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 開發者。
全部與 null
相等的元素,都爲空,沒問題;
數組、字符串、Arguments, 它們也能夠爲空,好比 length 屬性爲 0 的時候;
最後,用自帶的 _.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 數組去重
歡迎來個人博客交流。