摘要:返回值是一個新數組,思路也很清楚,對于已經排好序的數組,用后一個和前一個相比,不一樣就到中,對于沒有排好序的數組,要用到函數對是否包含元素進行判斷。
前面已經介紹過了,關于 _ 在內部是一個什么樣的情況,其實就是定義了一個名字叫做 _ 的函數,函數本身就是對象呀,就在 _ 上擴展了 100 多種方法。
起個頭接著上一篇文章的內容往下講,第一個擴展的函數是 each 函數,這和數組的 forEach 函數很像,即使不是數組,是偽數組,也可以通過 call 的方式來解決循環遍歷,forEach 接受三個參數,且沒有返回值,不對原數組產生改變。來看看 each 函數:
_.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 函數。
isArrayLike 前面已經介紹過了,不同于數組的 forEach 方法,_ 的 each 方法可以處理對象,只不過要先調用 _.keys 方法獲取對象的 keys 集合。返回值也算是一個特點吧,each 函數返回 obj,而數組的方法,是沒有返回值的。
第二個是 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); } return results; };
套路都是一樣的,既可以處理數組,又可以處理對象,但是 map 函數要有一個返回值,無論是數組,還是對象,返回值是一個數組,而且從代碼可以看到,新生成了數組,不會對原數組產生影響。
然后就是 reduce 函數,感覺介紹完這三個就可以召喚神龍了,其中 reduce 分為左和右,如下:
_.reduce = _.foldl = _.inject = createReduce(1); _.reduceRight = _.foldr = createReduce(-1);
為了減少代碼量,就用 createReduce 函數,接收 1 和 -1 參數:
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 方法是一樣的。
個人認為,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 更詳細,但是函數拆分太多了。
其他有意思的 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 開發者。
所有與 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 數組去重
歡迎來我的博客交流。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82777.html
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。今天就跟大家聊一聊中一些常用類型檢查方法,以及一些工具類的判斷方法。用是否含有屬性來判斷工具類判斷方法接下來看下一些常用的工具類判斷方法。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話...
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。后文中均假設比較的兩個參數為和。,如果和均是類型或者類型,我們可以用來判斷是否。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。為什么是 underscore?最主要的原...
摘要:直接來看例子一目了然,第一個參數是對象,第二個參數可以是一系列的值,也可以是數組數組中含,也可以是迭代函數,我們根據值,或者迭代函數來過濾中的鍵值對,返回新的對象副本。 Why underscore 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到很多。...
摘要:而數組元素去重是基于運算符的。而如果有迭代函數,則計算傳入迭代函數后的值,對值去重,調用方法,而該方法的核心就是調用方法,和我們上面說的方法一異曲同工。 Why underscore (覺得這部分眼熟的可以直接跳到下一段了...) 最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。 閱讀一些著名框架類庫的源碼,就好像...
摘要:最近開始看源碼,并將源碼解讀放在了我的計劃中。相對于其他源碼解讀的文章,基本都會從整體設計開始講起,樓主覺得這個庫有點特殊,決定按照自己的思路,從用代替說起。源碼沒有出現注意,其實有出現一處,是為,而不是,而用代替之。 Why underscore 最近開始看 underscore源碼,并將 underscore源碼解讀 放在了我的 2016計劃 中。 閱讀一些著名框架類庫的源碼,就好...
閱讀 2577·2021-10-25 09:45
閱讀 1239·2021-10-14 09:43
閱讀 2297·2021-09-22 15:23
閱讀 1519·2021-09-22 14:58
閱讀 1934·2019-08-30 15:54
閱讀 3539·2019-08-30 13:00
閱讀 1354·2019-08-29 18:44
閱讀 1571·2019-08-29 16:59