Lodash一直是我很喜歡用的一個庫,代碼也十分簡潔優美,一直想抽時間好好分析一下Lodash的源代碼。最近抽出早上的一些時間來分析一下Lodash的一些我以爲比較好的源碼。由於函數之間可能會有相互依賴,因此不會按照文檔順序進行分析,而是根據依賴關係和簡易程度由淺入深地進行分析。由於我的能力有限,若是理解有誤差,還請直接指出,以便我及時修改。javascript
源碼都是針對4.17.4
版本的,源docs寫得也很好,還有不少樣例。java
_.after
_.after
函數幾乎是Lodash中最容易理解的一個函數了,它一共有兩個參數,第一個參數是調用次數n
,第二個參數是n
次調用以後執行的函數func
。web
function after(n, func) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } n = toInteger(n); return function() { if (--n < 1) { return func.apply(this, arguments); } }; }
這個函數的核心代碼就是:數組
func.apply(this,arguments);
可是必定要注意,這個函數中有閉包的應用,就是這個參數n
。n
本應該在函數_.after
返回的時候就應該從棧空間回收,但事實上它還被返回的函數引用着,一直在內存中:閉包
return function() { if (--n < 1) { return func.apply(this, arguments); } };
因此一直到返回的函數執行完畢,n
所佔用的內存空間都沒法被回收。app
咱們再來看看這個apply
函數,咱們知道apply
函數能夠改變函數運行時的做用域,那麼問題來了,_.after
中func.apply
函數的this
究竟是誰呢?其實這個東西咱們沒有辦法從源碼中看出來,由於this
是在運行時決定的。那麼this
會變嗎?若是會的話怎麼變呢?要知道這個問題的答案,咱們須要先弄懂_.after
函數怎麼用。ide
_.after
函數調用後返回了另外一個函數,因此對於_.after
函數的返回值,咱們是須要再次調用的。因此最好的場景多是在延遲加載等場景中。固然爲了簡單起見我給出一個很簡單的例子:函數
const _ = require("lodash"); function foo(func ){ console.log("invoked foo."); func(); } var done = _.after(2,function bar(){ console.log("invoke bar"); }); for( var i = 0; i < 4; i++ ){ foo(done); }
正如咱們前面說的,n
的做用域是_.after
函數內部,因此在執行過程當中n
會一直遞減,所以輸出結果應該是在調用兩次foo
以後調用一次bar
,以後每次調用foo
,都會調用一次bar
。結果和咱們預期的一致:性能
invoked foo invoked foo invoke bar invoked foo invoke bar invoked foo invoke bar
那麼咱們再看看this
指向的問題,咱們修改一下上面的調用函數,讓bar
函數輸出一下內部的this
的一些屬性:ui
const _ = require("lodash"); function foo(func ){ this.name = "foo"; console.log("invoked foo: " + this.name ); func(); } var done = _.after(2,function bar(){ console.log("invoke bar: " + this.name); }); for( var i = 0; i < 4; i++ ){ foo(done); }
其實想來你們也應該可以猜到,在bar
函數中輸出的this.name
也是foo
:
invoked foo: foo invoked foo: foo invoke bar: foo invoked foo: foo invoke bar: foo invoked foo: foo invoke bar: foo
這是由於bar
的this
應該指向的是_.after
建立的函數的this
,而這個函數是window調用的,所以this
實際上指向就是window
,可是爲何會輸出foo
呢?由於foo函數的調用者也是window,而在foo函數中,將window.name設置成了foo
,因此bar
函數輸出的也是foo
(多謝評論指出!)。
_.map
_.map
函數咱們幾乎隨處可見,這個函數應用也至關普遍。
function map(collection, iteratee) { var func = isArray(collection) ? arrayMap : baseMap; return func(collection, getIteratee(iteratee, 3)); }
爲了簡化問題,咱們分析比較簡單的狀況:用一個func函數處理數組。
_.map([1,2,3],func);
在處理數組的時候,lodash是分開處理的,對於Array
採用arrayMap
進行處理,對於對象則採用baseMap
進行處理。
咱們先看數組arrayMap
:
function arrayMap(array, iteratee) { var index = -1, length = array == null ? 0 : array.length, result = Array(length); while (++index < length) { result[index] = iteratee(array[index], index, array); } return result; }
這個函數是一個私有函數,第一個參數是一個須要遍歷的數組,第二個參數是在遍歷過程中進行處理的函數;返回一個進行map處理以後的函數。
在看咱們須要進行遍歷處理的函數iteratee
,這個函數式經過getIteratee
函數獲得的:
function getIteratee() { var result = lodash.iteratee || iteratee; result = result === iteratee ? baseIteratee : result; return arguments.length ? result(arguments[0], arguments[1]) : result; }
若是lodash.iteratee
被從新定義,則使用用戶定義的iteratee
,不然就用官方定義的baseIteratee
。須要強調的是,result(arguments[0],arguments[1])
是柯里化的函數返回,返回的仍舊是一個函數。不可避免地,咱們須要看看官方定義的baseIteratee
的實現:
function baseIteratee(value) { // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. if (typeof value == 'function') { return value; } if (value == null) { return identity; } if (typeof value == 'object') { return isArray(value) ? baseMatchesProperty(value[0], value[1]) : baseMatches(value); } return property(value); }
咱們能夠看出來,這個iteratee
迭代者其實就是一個函數,在_.map
中getIteratee(iteratee, 3)
,給了兩個參數,按照邏輯,最終返回的是一個baseIteratee
,baseIteratee
的第一個參數value
就是iteratee
,這是一個函數,因此,baseIteratee
函數在第一個判斷就返回了。
因此咱們能夠將map函數簡化爲以下版本:
function map(collection,iteratee){ return arrayMap(collection,getIteratee(iteratee,3)); } function arrayMap(array, iteratee) { var index = -1, length = array == null ? 0 : array.length, result = Array(length); while (++index < length) { result[index] = iteratee(array[index], index, array); } return result; } function getIteratee() { var result = baseIteratee; return arguments.length ? result(arguments[0], arguments[1]) : result; } function baseIteratee(value) { if (typeof value == 'function') { return value; } }
能夠看到,最終調用函數func
的時候會傳入3個參數。array[index],index,array
。咱們能夠實驗,將func
實現以下:
function func(){ console.log(「arguments[0] 」 + arguments[0]); console.log(「arguments[1] 」 + arguments[1]); console.log(「arguments[2] 」 + arguments[2]); console.log("-----") }
輸出的結果也和咱們的預期同樣,輸出的第一個參數是該列表元素自己,第二個參數是數組下標,第三個參數是整個列表:
arguments[0] 6 arguments[1] 0 arguments[2] 6,8,10 ----- arguments[0] 8 arguments[1] 1 arguments[2] 6,8,10 ----- arguments[0] 10 arguments[1] 2 arguments[2] 6,8,10 ----- [ undefined, undefined, undefined ]
上面的分析就是拋磚引玉,先給出數組的分析,別的非數組,例如對象的遍歷處理則會走到別的分支進行處理,各位看官有興趣能夠深刻研究。
_.ary
這個函數是用來限制參數個數的。這個函數咋一看好像沒有什麼用,但咱們考慮以下場景,將一個字符列表['6','8','10']
轉爲整型列表[6,8,10]
,用_.map
實現,咱們天然而然會寫出這樣的代碼:
const _ = require("lodash"); _.map(['6','8','10'],parseInt);
好像很完美,咱們輸出看看:
[ 6, NaN, 2 ]
很詭異是否是,看看內部到底發生了什麼?其實看了上面的-.map
函數的分析,其實緣由已經很明顯了。對於parseInt
函數而言,其接收兩個參數,第一個是須要處理的字符串,第二個是進制:
/** * @param string 必需。要被解析的字符串。 * @param radix * 可選。表示要解析的數字的基數。該值介於 2 ~ 36 之間。 * 若是省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。若是它以 「0x」 或 「0X」 開頭,將以 16 爲基數。 * 若是該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN */ parseInt(string, radix) /** 當參數 radix 的值爲 0,或沒有設置該參數時,parseInt() 會根據 string 來判斷數字的基數。 舉例,若是 string 以 "0x" 開頭,parseInt() 會把 string 的其他部分解析爲十六進制的整數。若是 string 以 0 開頭,那麼 ECMAScript v3 容許 parseInt() 的一個實現把其後的字符解析爲八進制或十六進制的數字。若是 string 以 1 ~ 9 的數字開頭,parseInt() 將把它解析爲十進制的整數。 */
那麼這樣的輸出也就不難理解了:
處理第一個數組元素6的時候,parseInt
實際傳入參數(6,0)
,那麼按照十進制解析,會獲得6
,處理第二個數組元素的時候傳入的實際參數是(8,1)
,返回NaN
,對於第三個數組元素,按照2進制處理,則10
返回的是2
。
因此在上述需求的時候咱們須要限制參數的個數,這個時候_.ary
函數就登場了,上面的函數這樣處理就沒有問題了:
const _ = require("lodash"); _.map(['6','8','10'],_.ary(parseInt,1));
咱們看看這個函數是怎麼實現的:
function ary(func, n, guard) { n = guard ? undefined : n; n = (func && n == null) ? func.length : n; return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); }
這個函數先檢查n
的值,須要說明的是func.length
返回的是函數的聲明參數個數。而後返回了一個createWrap
包裹函數,這個函數能夠說是髒活累活處理工廠了,負責不少函數的包裹處理工做,並且爲了提高性能,還將不一樣的判斷用bitflag
進行與/非處理,能夠說是很用盡心機了。
/** * Creates a function that either curries or invokes `func` with optional * `this` binding and partially applied arguments. * * @private * @param {Function|string} func The function or method name to wrap. * @param {number} bitmask The bitmask flags. * 1 - `_.bind` 1 0b0000000000000001 * 2 - `_.bindKey` 0b0000000000000010 * 4 - `_.curry` or `_.curryRight`... 0b0000000000000100 * 8 - `_.curry` 0b0000000000001000 * 16 - `_.curryRight` 0b0000000000010000 * 32 - `_.partial` 0b0000000000100000 * 64 - `_.partialRight` 0b0000000001000000 * 128 - `_.rearg` 0b0000000010000000 * 256 - `_.ary` 0b0000000100000000 * 512 - `_.flip` 0b0000001000000000 * @param {*} [thisArg] The `this` binding of `func`. * @param {Array} [partials] The arguments to be partially applied. * @param {Array} [holders] The `partials` placeholder indexes. * @param {Array} [argPos] The argument positions of the new function. * @param {number} [ary] The arity cap of `func`. * @param {number} [arity] The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; if (!isBindKey && typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } var length = partials ? partials.length : 0; if (!length) { bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); partials = holders = undefined; } ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); arity = arity === undefined ? arity : toInteger(arity); length -= holders ? holders.length : 0; if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { var partialsRight = partials, holdersRight = holders; partials = holders = undefined; } var data = isBindKey ? undefined : getData(func); var newData = [ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity ]; if (data) { mergeData(newData, data); } func = newData[0]; bitmask = newData[1]; thisArg = newData[2]; partials = newData[3]; holders = newData[4]; arity = newData[9] = newData[9] === undefined ? (isBindKey ? 0 : func.length) : nativeMax(newData[9] - length, 0); if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); } if (!bitmask || bitmask == WRAP_BIND_FLAG) { var result = createBind(func, bitmask, thisArg); } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { result = createCurry(func, bitmask, arity); } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { result = createPartial(func, bitmask, thisArg, partials); } else { result = createHybrid.apply(undefined, newData); } var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); }
看上去太複雜了,把無關的代碼削減掉:
function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { // 0000000100000000 & 0000000000000010 // var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; var isBindKey = 0; var length = 0; // if (!length) { // 0000000000100000 | 0000000001000000 // ~(0000000001100000) // 1111111110011111 // &0000000100000000 // 0000000100000000 = WRAP_ARY_FLAG // bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); // bitmask = WRAP_ARY_FLAG; // partials = holders = undefined; // } bitmask = WRAP_ARY_FLAG; partials = holders = undefined; ary = undefined; arity = arity === undefined ? arity : toInteger(arity); // because holders == undefined //length -= 0; // because isBindKey == 0 // var data = isBindKey ? undefined : getData(func); var data = getData(func); var newData = [ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity ]; if (data) { mergeData(newData, data); } func = newData[0]; bitmask = newData[1]; thisArg = newData[2]; partials = newData[3]; holders = newData[4]; arity = newData[9] = newData[9] === undefined ? func.length : newData[9]; result = createHybrid.apply(undefined, newData); var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); }
簡化了一些以後咱們來到了createHybrid
函數,這個函數也巨複雜,因此咱們仍是按照簡化方法,把咱們用不到的邏輯給簡化:
function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { var isAry = bitmask & WRAP_ARY_FLAG, isBind = bitmask & WRAP_BIND_FLAG, isBindKey = bitmask & WRAP_BIND_KEY_FLAG, isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), isFlip = bitmask & WRAP_FLIP_FLAG, Ctor = isBindKey ? undefined : createCtor(func); function wrapper() { var length = arguments.length, args = Array(length), index = length; while (index--) { args[index] = arguments[index]; } if (isCurried) { var placeholder = getHolder(wrapper), holdersCount = countHolders(args, placeholder); } if (partials) { args = composeArgs(args, partials, holders, isCurried); } if (partialsRight) { args = composeArgsRight(args, partialsRight, holdersRight, isCurried); } length -= holdersCount; if (isCurried && length < arity) { var newHolders = replaceHolders(args, placeholder); return createRecurry( func, bitmask, createHybrid, wrapper.placeholder, thisArg, args, newHolders, argPos, ary, arity - length ); } var thisBinding = isBind ? thisArg : this, fn = isBindKey ? thisBinding[func] : func; length = args.length; if (argPos) { args = reorder(args, argPos); } else if (isFlip && length > 1) { args.reverse(); } if (isAry && ary < length) { args.length = ary; } if (this && this !== root && this instanceof wrapper) { fn = Ctor || createCtor(fn); } return fn.apply(thisBinding, args); } return wrapper; }
把不須要的邏輯削減掉:
function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { var isAry = 1; function wrapper() { var length = arguments.length, args = Array(length), index = length; while (index--) { args[index] = arguments[index]; } var thisBinding = this, fn = func; length = args.length; if (isAry && ary < length) { args.length = ary; } return fn.apply(thisBinding, args); } return wrapper; }
好了,繞了一大圈,終於看到最終的邏輯了,_.ary
函數其實就是把參數列表從新賦值了一下,並進行了長度限制。想一想這個函數實在是太麻煩了,咱們本身能夠根據這個邏輯實現一個簡化版的_.ary
:
function ary(func,n){ return function(){ var length = arguments.length, args = Array(length), index = length; while(index--){ args[index] = arguments[index]; } args.length = n; return func.apply(this,args); } }
試試效果:
console.log(_.map(['6','8','10'],ary(parseInt,1)));
工做得很不錯:
[ 6, 8, 10 ]
今天分析這三個函數就花了一成天的時間,可是收穫頗豐,可以靜下心來好好分析一個著名的開源庫,並可以理解透裏面的一些邏輯,確實是一件頗有意思的事情。我會在有時間的時候把Lodash這個我很喜歡的庫都好好分析一遍,盡我最大的努力將裏面的邏輯表述清楚,但願可以簡明易懂。
最後,最晚下週一將會更新第二篇分析文章,敬請期待。
© 版權全部,未經容許不得轉載,宣傳一下我的博客 chenquan.me