摘要:今天要講的,是我從的源碼實現文件中學到的幾個很基礎,卻又容易被忽略的知識點。在函數式編程中,函數是一等公民,它可以只是根據參數,做簡單的組合操作,再作為別的函數的返回值。所以,閱讀源碼,是一種很棒的重溫基礎知識的方式。
前言
上一篇文章 「前端面試題系列8」數組去重(10 種濃縮版) 的最后,簡單介紹了 lodash 中的數組去重方法 _.uniq,它可以實現我們日常工作中的去重需求,能夠去重 NaN,并保留 {...}。
今天要講的,是我從 _.uniq 的源碼實現文件 baseUniq.js 中學到的幾個很基礎,卻又容易被忽略的知識點。
三個 API讓我們先從三個功能相近的 API 講起,他們分別是:_.uniq、_.uniqBy、_.uniqWith。它們三個背后的實現文件,都指向了 .internal 下的 baseUniq.js。
區別在于 _.uniq 只需傳入一個源數組 array, _.uniqBy 相較于 _.uniq 要多傳一個迭代器 iteratee,而 _.uniqWith 要多傳一個比較器 comparator。iteratee 和 comparator 的用法,會在后面說到。
以 _.uniqWith 為例,它是這樣調用 _.baseUniq 的:
function uniqWith(array, comparator) { comparator = typeof comparator == "function" ? comparator : undefined return (array != null && array.length) ? baseUniq(array, undefined, comparator) : [] }baseUniq 的實現原理
baseUniq 的源碼并不多,但比較繞。先貼一下的源碼。
const LARGE_ARRAY_SIZE = 200 function baseUniq(array, iteratee, comparator) { let index = -1 let includes = arrayIncludes let isCommon = true const { length } = array const result = [] let seen = result if (comparator) { isCommon = false includes = arrayIncludesWith } else if (length >= LARGE_ARRAY_SIZE) { const set = iteratee ? null : createSet(array) if (set) { return setToArray(set) } isCommon = false includes = cacheHas seen = new SetCache } else { seen = iteratee ? [] : result } outer: while (++index < length) { let value = array[index] const computed = iteratee ? iteratee(value) : value value = (comparator || value !== 0) ? value : 0 if (isCommon && computed === computed) { let seenIndex = seen.length while (seenIndex--) { if (seen[seenIndex] === computed) { continue outer } } if (iteratee) { seen.push(computed) } result.push(value) } else if (!includes(seen, computed, comparator)) { if (seen !== result) { seen.push(computed) } result.push(value) } } return result }
為了兼容剛才說的三個 API,就產生了不少的干擾項。如果先從 _.uniq 入手,去掉 iteratee 和 comparator 的干擾,就會清晰不少。
function baseUniq(array) { let index = -1 const { length } = array const result = [] if (length >= 200) { const set = createSet(array) return setToArray(set) } outer: while (++index < length) { const value = array[index] if (value === value) { let resultIndex = result.length while (resultIndex--) { if (result[resultIndex] === value) { continue outer } } result.push(value) } else if (!includes(seen, value)) { result.push(value) } } return result }
這里有 2 個知識點。
知識點一、NaN === NaN 嗎?在源碼中有一個判斷 value === value,乍一看,會覺得這是句廢話!?!但其實,這是為了過濾 NaN 的情況。
MDN 中對 NaN 的解釋是:它是一個全局對象的屬性,初始值就是 NaN。它通常都是在計算失敗時,作為 Math 的某個方法的返回值出現的。
判斷一個值是否是 NaN,必須使用 Number.isNaN() 或 isNaN(),在執行自比較之中:NaN,也只有 NaN,比較之中不等于它自己。
NaN === NaN; // false Number.NaN === NaN; // false isNaN(NaN); // true isNaN(Number.NaN); // true
所以,在源碼中,當遇到 NaN 的情況時,baseUniq 會轉而去執行 !includes(seen, value) 的判斷,去處理 NaN 。
知識點二、冒號的特殊作用在源碼的主體部分,while 語句之前,有一行 outer:,它是干什么用的呢? while 中還有一個 while 的內部,有一行 continue outer,從語義上理解,好像是繼續執行 outer,這又是種什么寫法呢?
outer: while (++index < length) { ... while (resultIndex--) { if (result[resultIndex] === value) { continue outer } } }
我們都知道 Javascript 中,常用到冒號的地方有三處,分別是:A ? B : C 三元操作符、switch case 語句中、對象的鍵值對組成。
但其實還有一種并不常見的特殊作用:標簽語句。在 Javascript 中,任何語句都可以通過在它前面加上標志符和冒號來標記(identifier: statement),這樣就可以在任何地方使用該標記,最常用于循環語句中。
所以,在源碼中,outer 只是看著有點不習慣,多看兩遍就好了,語義上還是很好理解的。
_.uniqBy 的 iteratee_.uniqBy 可根據指定的 key 給一個對象數組去重,一個官網的例子如下:
// The `_.property` iteratee shorthand. _.uniqBy([{ "x": 1 }, { "x": 2 }, { "x": 1 }], "x"); // => [{ "x": 1 }, { "x": 2 }]
這里的 "x" 是 _.property("x") 的縮寫,它指的就是 iteratee。
從給出的例子和語義上看,還挺好理解的。但是為什么 _.property 就能實現對象數組的去重了呢?它又是如何實現的呢?
@param {Array|string} path The path of the property to get. @returns {Function} Returns the new accessor function. function property(path) { return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path) }
從注釋看,property 方法會返回一個 Function,再看 baseProperty 的實現:
@param {string} key The key of the property to get. @returns {Function} Returns the new accessor function. function baseProperty(key) { return (object) => object == null ? undefined : object[key] }
咦?怎么返回的還是個 Function ?感覺它什么也沒干呀,那個參數 object 又是哪里來的?
知識點三、純函數的概念純函數,是函數式編程中的概念,它代表這樣一類函數:對于指定輸出,返回指定的結果。不存在副作用。
// 這是一個簡單的純函數 const addByOne = x => x + 1;
也就是說,純函數的返回值只依賴其參數,函數體內不能存在任何副作用。如果是同樣的參數,則一定能得到一致的返回結果。
function baseProperty(key) { return (object) => object == null ? undefined : object[key] }
baseProperty 返回的就是一個純函數,在符合條件的情況下,輸出 object[key]。在函數式編程中,函數是“一等公民”,它可以只是根據參數,做簡單的組合操作,再作為別的函數的返回值。
所以,在源碼中,object 是調用 baseProperty 時傳入的對象。 baseProperty 的作用,是返回期望結果為 object[key] 的函數。
_.uniqWith 的 comparator還是先從官網的小例子說起,它會完全地給對象中所有的鍵值對,進行比較。
var objects = [{ "x": 1, "y": 2 }, { "x": 2, "y": 1 }, { "x": 1, "y": 2 }]; _.uniqWith(objects, _.isEqual); // => [{ "x": 1, "y": 2 }, { "x": 2, "y": 1 }]
而在 baseUniq 的源碼中,可以看到最終的實現,需要依賴 arrayIncludesWith 方法,以下是它的源碼:
function arrayIncludesWith(array, target, comparator) { if (array == null) { return false } for (const value of array) { if (comparator(target, value)) { return true } } return false }
arrayIncludesWith 沒什么復雜的。comparator 作為一個參數傳入,將 target 和 array 的每個 value 進行處理。從官網的例子看,_.isEqual 就是 comparator,就是要比較它們是否相等。
接著就追溯到了 _.isEqual 的源碼,它的實現文件是 baseIsEqualDeep.js。在里面看到一個讓我犯迷糊的寫法,這是一個判斷。
/** Used to check objects for own properties. */ const hasOwnProperty = Object.prototype.hasOwnProperty ... const objIsWrapped = objIsObj && hasOwnProperty.call(object, "__wrapped__")
hasOwnProperty ?call, "__wrapped__" ?
知識點四、對象的 hasOwnProperty再次查找到了 MDN 的解釋:所有繼承了 Object 的對象都會繼承到 hasOwnProperty 方法。它可以用來檢測一個對象是否含有特定的自身屬性;會忽略掉那些從原型鏈上繼承到的屬性。
o = new Object(); o.prop = "exists"; o.hasOwnProperty("prop"); // 返回 true o.hasOwnProperty("toString"); // 返回 false o.hasOwnProperty("hasOwnProperty"); // 返回 false
call 的用法可以參考這篇 細說 call、apply 以及 bind 的區別和用法。
那么 hasOwnProperty.call(object, "__wrapped__") 的意思就是,判斷 object 這個對象上是否存在 "__wrapped__" 這個自身屬性。
wrapped 是什么屬性?這就要說到 lodash 的延遲計算方法 _.chain,它是一種函數式風格,從名字就可以看出,它實現的是一種鏈式的寫法。比如下面這個例子:
var names = _.chain(users) .map(function(user){ return user.user; }) .join(" , ") .value();
如果你沒有顯樣的調用value方法,使其立即執行的話,將會得到如下的LodashWrapper延遲表達式:
LodashWrapper {__wrapped__: LazyWrapper, __actions__: Array[1], __chain__: true, constructor: function, after: function…}
因為延遲表達式的存在,因此我們可以多次增加方法鏈,但這并不會被執行,所以不會存在性能的問題,最后直到我們需要使用的時候,使用 value() 顯式立即執行即可。
所以,在 baseIsEqualDeep 源碼中,才需要做 hasOwnProperty 的判斷,然后在需要的情況下,執行 object.value()。
總結閱讀源碼,在一開始會比較困難,因為會遇到一些看不明白的寫法。就像一開始我卡在了 value === value 的寫法,不明白它的用意。一旦知道了是為了過濾 NaN 用的,那后面就會通暢很多了。
所以,閱讀源碼,是一種很棒的重溫基礎知識的方式。遇到看不明白的點,不要放過,多查多問多看,才能不斷地夯實基礎,讀懂更多的源碼思想,體會更多的原生精髓。如果我在一開始看到 value === value 時就放棄了,那或許就不會有今天的這篇文章了。
PS:歡迎關注我的公眾號 “超哥前端小棧”,交流更多的想法與技術。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109206.html
摘要:用于檢測自己是否在自己的原型鏈上如果是函數,則取出該函數的原型對象否則,取出對象的原型對象其中,的判斷,是為了確定的類型是對象或數組。相當于,而的構造函數是一個函數對象。 showImg(https://segmentfault.com/img/bVbq2N1?w=640&h=437); 前言 接著上一篇文章 lodash 是如何實現深拷貝的(上),今天會繼續解讀 _.cloneDee...
摘要:前言熟悉的朋友想必都使用或者聽說過,算是一個元老級的庫了,從第一個版本發布到現在,已經有年時間了。中緩存是默認開啟的,同時也可以設置為禁用。處理屏屏幕模糊的問題,直接給出處理方法,就不展開說了。 前言 熟悉 canvas 的朋友想必都使用或者聽說過 Fabric.js,Fabric 算是一個元老級的 canvas 庫了,從第一個版本發布到現在,已經有 8 年時間了。我近一年時間也在項目...
摘要:上對位運算的解釋是它經常被用來創建處理以及讀取標志位序列一種類似二進制的變量。位運算,常用于處理同時存在多個布爾選項的情形。掩碼中的每個選項的值都是的冪,位運算是位的。位運算,說白了就是直接對某個數據在內存中的二進制位,進行運算操作。 showImg(https://segmentfault.com/img/bVbrC56?w=2208&h=1242); 前言 上一篇文章 「前端面試題...
摘要:而在構造函數中,返回了的實例對象。在中直接返回過的實例,這里的是的真正構造函數最后對外暴露入口時,將字符與對等起來。因此當我們直接使用創建一個對象時,實際上是創建了一個的實例,這里的正真構造函數是原型中的方法。 showImg(https://segmentfault.com/img/remote/1460000008749398); 早幾年學習前端,大家都非常熱衷于研究jQuery源...
閱讀 2481·2023-04-25 21:41
閱讀 1653·2021-09-22 15:17
閱讀 1926·2021-09-22 10:02
閱讀 2438·2021-09-10 11:21
閱讀 2581·2019-08-30 15:53
閱讀 1001·2019-08-30 15:44
閱讀 953·2019-08-30 13:46
閱讀 1139·2019-08-29 18:36