摘要:返回值是一個新數(shù)組,思路也很清楚,對于已經(jīng)排好序的數(shù)組,用后一個和前一個相比,不一樣就到中,對于沒有排好序的數(shù)組,要用到函數(shù)對是否包含元素進(jìn)行判斷。
前面已經(jīng)介紹過了,關(guān)于 _ 在內(nèi)部是一個什么樣的情況,其實就是定義了一個名字叫做 _ 的函數(shù),函數(shù)本身就是對象呀,就在 _ 上擴(kuò)展了 100 多種方法。
起個頭接著上一篇文章的內(nèi)容往下講,第一個擴(kuò)展的函數(shù)是 each 函數(shù),這和數(shù)組的 forEach 函數(shù)很像,即使不是數(shù)組,是偽數(shù)組,也可以通過 call 的方式來解決循環(huán)遍歷,forEach 接受三個參數(shù),且沒有返回值,不對原數(shù)組產(chǎn)生改變。來看看 each 函數(shù):
_.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 函數(shù)接收三個參數(shù),分別是 obj 執(zhí)行體,回調(diào)函數(shù)和回調(diào)函數(shù)的上下文,回調(diào)函數(shù)會通過 optimizeCb 來優(yōu)化,optimizeCb 沒有傳入第三個參數(shù) argCount,表明默認(rèn)是三個,但是如果上下文 context 為空的情況下,就直接返回 iteratee 函數(shù)。
isArrayLike 前面已經(jīng)介紹過了,不同于數(shù)組的 forEach 方法,_ 的 each 方法可以處理對象,只不過要先調(diào)用 _.keys 方法獲取對象的 keys 集合。返回值也算是一個特點吧,each 函數(shù)返回 obj,而數(shù)組的方法,是沒有返回值的。
第二個是 map 函數(shù):
_.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); // 回調(diào)函數(shù) var keys = !isArrayLike(obj) && _.keys(obj), // 處理非數(shù)組 length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; // 數(shù)組或者對象 results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; };
套路都是一樣的,既可以處理數(shù)組,又可以處理對象,但是 map 函數(shù)要有一個返回值,無論是數(shù)組,還是對象,返回值是一個數(shù)組,而且從代碼可以看到,新生成了數(shù)組,不會對原數(shù)組產(chǎn)生影響。
然后就是 reduce 函數(shù),感覺介紹完這三個就可以召喚神龍了,其中 reduce 分為左和右,如下:
_.reduce = _.foldl = _.inject = createReduce(1); _.reduceRight = _.foldr = createReduce(-1);
為了減少代碼量,就用 createReduce 函數(shù),接收 1 和 -1 參數(shù):
function createReduce(dir) { // iterator 函數(shù)是執(zhí)行,在最終結(jié)果里面 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) { // 依舊是回調(diào)函數(shù),優(yōu)化 iteratee = optimizeCb(iteratee, context, 4); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, index = dir > 0 ? 0 : length - 1; // 參數(shù)可以忽略第一個值,但用數(shù)組的第一個元素替代 if (arguments.length < 3) { memo = obj[keys ? keys[index] : index]; index += dir; } return iterator(obj, iteratee, memo, keys, index, length); }; }
createReduce 用閉包返回了一個函數(shù),該函數(shù)接受四個參數(shù),分別是執(zhí)行數(shù)組或?qū)ο?、回調(diào)函數(shù)、初始值和上下文,個人感覺這里的邏輯有點換混亂,比如我只有三個參數(shù),有初始值沒有上下文,這個好辦,但是如果同樣是三個參數(shù),我是有上下文,但是沒有初始值,就會導(dǎo)致流程出現(xiàn)問題。不過我也沒有比較好的解決辦法。
當(dāng)參數(shù)為兩個的時候,初始值沒有,就會調(diào)用數(shù)組或?qū)ο蟮牡谝粋€參數(shù)作為初始值,并把指針后移一位(這里用指針,其實是數(shù)組的索引),傳入的函數(shù),它有四個參數(shù),這和數(shù)組 reduce 方法是一樣的。
個人認(rèn)為,reduce 用來處理對象,還是有點問題的,比如獲取對象的 keys 值,如果每次獲取的順序都不一樣,導(dǎo)致處理的順序也不一樣,那最終的結(jié)果還會一樣嗎?所以我決定處理對象還是要謹(jǐn)慎點好。
_.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 也是一個已經(jīng)在數(shù)組方法中實現(xiàn)的,對應(yīng)于數(shù)組的 find 和 findIndex 函數(shù)。在 _中,_.findKey 針對于對象,_.findIndex 針對于數(shù)組,又略有不同,但是討論和 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);有重點的來看
后面覺得有的函數(shù)真的是太無聊了,套路都是一致的,仔細(xì)看了也學(xué)不到太多的東西,感覺還是有選擇的來聊聊吧。underscore-analysis,這篇博客里的內(nèi)容寫得挺不錯的,很多內(nèi)容都一針見血,準(zhǔn)備按照博客中的思路來解讀源碼,不打算一步一步來了,太無聊。
類型判斷jQuery 里面有一個判斷類型的函數(shù),就是 $.type,它最主要的好處就是一個函數(shù)可以對所以的類型進(jìn)行判斷,然后返回類型名。_ 中的判斷略坑,函數(shù)很多,而且都是以 is 開頭,什么 isArray,isFunction等等。
var toString = Object.prototype.toString, nativeIsArray = Array.isArray; _.isArray = nativeIsArray || function(obj) { return toString.call(obj) === "[object Array]"; };
可以看得出來,設(shè)計者的心思還是挺仔細(xì)的,當(dāng)然,還有:
_.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 不一樣,但是也是情理之中,很好理解,那么問題來了,這樣會不會很麻煩,光構(gòu)造這些函數(shù)就要花很久的時間吧,答案用下面的代碼來解釋:
_.each(["Arguments", "Function", "String", "Number", "Date", "RegExp", "Error"], function(name) { _["is" + name] = function(obj) { return toString.call(obj) === "[object " + name + "]"; }; });
對于一些不用特殊處理的函數(shù),直接用 each 函數(shù)來搞定。
除此之外,還有一些有意思的 is 函數(shù):
// 只能用來判斷 NaN 類型,因為只有 NaN !== NaN 成立,其他 Number 均不成立 _.isNaN = function(obj) { return _.isNumber(obj) && obj !== +obj; }; // null 嚴(yán)格等于哪些類型? _.isNull = function(obj) { return obj === null; }; // 又是一個嚴(yán)格判斷 === // 貌似 _.isUndefined() == true 空參數(shù)的情況也是成立的 _.isUndefined = function(obj) { return obj === void 0; };
不過對于 isNaN 函數(shù),還是有 bug 的,比如:
_.isNaN(new Number(1)); // true // new Number(1) 和 Number(1) 是有區(qū)別的
這邊 github issue 上已經(jīng)有人提出了這個問題,_.isNaN,也合并到分支了 Fixes _.isNaN for wrapped numbers,但是不知道為什么我這個 1.8.3 版本還是老樣子,難度我下載了一個假的 underscore?issue 中提供了解決辦法:
_.isNaN = function(obj) { // 將 !== 換成 != return _.isNumber(obj) && obj != +obj; };
我跑去最新發(fā)布的 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; }
比較了一下,發(fā)現(xiàn) jQuery 相比于 underscore,少了 Arguments 的判斷,多了 ES6 的 Symbol 的判斷(PS:underscore 好久沒人維護(hù)了?)。所以 jQuery 對 Arguments 的判斷只能返回 object,_ 中是沒有 _.isSymbol 函數(shù)的。以前一直看 jQuery 的類型判斷,竟然不知道 Arguments 也可以多帶帶分為一類 arguments。還有就是,如果讓我來選擇在項目中使用哪個,我肯定選擇 jQuery 的這種方式,盡管 underscore 更詳細(xì),但是函數(shù)拆分太多了。
其他有意思的 is 函數(shù)前面說了,underscore 給人一種很啰嗦的感覺,is 函數(shù)太多,話雖如此,總有幾個非常有意思的函數(shù):
_.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 用來判斷是否為空,我剛開始看到這個函數(shù)的時候,有點懵,說到底還是對 Empty 這個詞理解的不夠深刻。到底什么是 空 呢,看源碼,我覺得這是最好的答案,畢竟匯集了那么多優(yōu)秀多 JS 開發(fā)者。
所有與 null 相等的元素,都為空,沒問題;
數(shù)組、字符串、Arguments, 它們也可以為空,比如 length 屬性為 0 的時候;
最后,用自帶的 _.keys 判斷 obj key 集合的長度是否為 0。
有時候覺得看代碼,真的是一種升華。
還有一個 isElement,很簡單,只是不明白為什么用了兩次非來判斷:
_.isElement = function(obj) { // !! return !!(obj && obj.nodeType === 1); };
重點來說下 isEqual 函數(shù):
_.isEqual = function(a, b) { return eq(a, b); }; var eq = function(a, b, aStack, bStack) { // 解決 0 和 -0 不應(yīng)該相等的問題? // 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 構(gòu)造字符串 return "" + a === "" + b; case "[object Number]": // +a 可以將類型為 new Number 的 a 轉(zhuǎn)變?yōu)閿?shù)字 // +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; } // 如果以上都不能滿足,可能判斷的類型為數(shù)組或?qū)ο螅?== 是無法解決的 var areArrays = className === "[object Array]"; // 非數(shù)組的情況,看一下它們是否同祖先,不同祖先,failed if (!areArrays) { // 奇怪的數(shù)據(jù) 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--) { // 這個應(yīng)該是為了防止嵌套循環(huán) 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; // 數(shù)組長度都不等,肯定不一樣 if (length !== b.length) return false; // 遞歸比較,如果有一個不同,返回 false while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // 非數(shù)組的情況 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; } } // 清空數(shù)組 aStack.pop(); bStack.pop(); return true; // 一路到底,沒有失敗,則返回成功 };
總結(jié)一下,就是 _.isEqual 內(nèi)部雖然用的是 === 這種判斷,但是對于 === 判斷失敗的情況,isEqual 會嘗試將比較的元素拆分比較,比如,如果是兩個不同引用地址數(shù)組,它們元素都是一樣的,則返回 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 是一個非常有心機的函數(shù)。
數(shù)組去重關(guān)于數(shù)組去重,從面試筆試的程度來說,是家常便飯的題目,實際中也會經(jīng)常用到,前段時間看到一篇去重的博客,感覺含金量很高,地址在這:也談JavaScript數(shù)組去重,年代在久一點,就是玉伯大蝦的從 JavaScript 數(shù)組去重談性能優(yōu)化。
_ 中也有去重函數(shù) uniq 或者 unique:
_.uniq = _.unique = function(array, isSorted, iteratee, context) { // 和 jQuery 一樣,平移參數(shù) if (!_.isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false; } // 又是回調(diào) cb,三個參數(shù) 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; // 如果已經(jīng)排列好,就直接和前一個進(jìn)行比較 if (isSorted) { if (!i || seen !== computed) result.push(value); seen = computed; } else if (iteratee) { // seen 此時化身為一個去重數(shù)組,前提是有 iteratee 函數(shù) if (!_.contains(seen, computed)) { seen.push(computed); result.push(value); } } else if (!_.contains(result, value)) { result.push(value); } } return result; };
還是要從 unique 的幾個參數(shù)說起,第一個參數(shù)是數(shù)組,第二個表示是否已經(jīng)排好序,第三個參數(shù)是一個函數(shù),表示對數(shù)組的元素進(jìn)行怎樣的處理,第四個參數(shù)是第三個參數(shù)的上下文。返回值是一個新數(shù)組,思路也很清楚,對于已經(jīng)排好序的數(shù)組,用后一個和前一個相比,不一樣就 push 到 result 中,對于沒有排好序的數(shù)組,要用到 _.contains 函數(shù)對 result 是否包含元素進(jìn)行判斷。
去重的話,如果數(shù)組是排好序的,效率會很高,時間復(fù)雜度為 n,只要遍歷一次循環(huán)即刻,對于未排好序的數(shù)組,要頻繁的使用 contains 函數(shù),復(fù)雜度很高,平均為 n 的平方。去重所用到為相等為嚴(yán)格等于 ===,使用的時候要小心。
_.contains 函數(shù)如下所示:
_.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) { // 這里使用的是嚴(yán)格等于 if (array[idx] === item) return idx; // 找到,返回索引 } return -1; // 沒找到,返回 -1 }; }總結(jié)
感覺 Underscore 的源碼看起來還是很簡單的,Underscore 里面有一些過時的函數(shù),這些都可以拿過來學(xué)習(xí),邏輯比較清晰,并不像 jQuery 那樣,一個函數(shù)里面好多內(nèi)部函數(shù),看著看著就暈了。
參考Underscore.js (1.8.3) 中文文檔
常用類型判斷以及一些有用的工具方法
也談JavaScript數(shù)組去重
JavaScript 數(shù)組去重
歡迎來我的博客交流。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/82777.html
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。今天就跟大家聊一聊中一些常用類型檢查方法,以及一些工具類的判斷方法。用是否含有屬性來判斷工具類判斷方法接下來看下一些常用的工具類判斷方法。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話...
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。后文中均假設(shè)比較的兩個參數(shù)為和。,如果和均是類型或者類型,我們可以用來判斷是否。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學(xué)到很多。為什么是 underscore?最主要的原...
摘要:直接來看例子一目了然,第一個參數(shù)是對象,第二個參數(shù)可以是一系列的值,也可以是數(shù)組數(shù)組中含,也可以是迭代函數(shù),我們根據(jù)值,或者迭代函數(shù)來過濾中的鍵值對,返回新的對象副本。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學(xué)到很多。...
摘要:而數(shù)組元素去重是基于運算符的。而如果有迭代函數(shù),則計算傳入迭代函數(shù)后的值,對值去重,調(diào)用方法,而該方法的核心就是調(diào)用方法,和我們上面說的方法一異曲同工。 Why underscore (覺得這部分眼熟的可以直接跳到下一段了...) 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像...
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。相對于其他源碼解讀的文章,基本都會從整體設(shè)計開始講起,樓主覺得這個庫有點特殊,決定按照自己的思路,從用代替說起。源碼沒有出現(xiàn)注意,其實有出現(xiàn)一處,是為,而不是,而用代替之。 Why underscore 最近開始看 underscore源碼,并將 underscore源碼解讀 放在了我的 2016計劃 中。 閱讀一些著名框架類庫的源碼,就好...
閱讀 2598·2021-10-25 09:45
閱讀 1257·2021-10-14 09:43
閱讀 2314·2021-09-22 15:23
閱讀 1542·2021-09-22 14:58
閱讀 1946·2019-08-30 15:54
閱讀 3554·2019-08-30 13:00
閱讀 1371·2019-08-29 18:44
閱讀 1585·2019-08-29 16:59