摘要:眾所周知,函數能夠將一個集合進行折疊。我們看到源代碼是這樣的在官方的注釋中說,對于對象,遍歷順序是無法保證的。我在閱讀源代碼的過程中也會遇到很多不理解的地方。待續(xù)下周將繼續(xù)更新源碼分析系列,接下來將會分析集合方法。
前言
這是Lodash源碼分析的第二篇文章,我們在第一篇Lodash 源碼分析(一)“Function” Methods中介紹了基本的_.after,_.map,以及復雜的_.ary函數的實現以及我們自己的自定義輕量級版本。大概清楚了Lodash的整個代碼脈絡。這次我們繼續(xù)分析,這次我們講講_.reduce和_.curry。
_.reduce我一直覺得,如果能夠理解_.map和_.reduce的實現,那么任何復雜的函數都不在話下。我們已經介紹了_.map的實現,知道了_.map函數中是如何處理集合,并將其逐個進行函數處理的。我們知道在_.map函數中會把三個參數傳到給定的函數中,分別是array[index],index和array。這次我們看看_.reduce函數。
眾所周知,_.reduce函數能夠將一個集合進行"折疊"。"折疊"理解起來比較抽象。我們可以通過代碼作為樣例說明一下:
const _ = require("lodash"); _.reduce([1,2,3],function(a,b){return a+b}); // 6
如果你不知道_.reduce到底是怎么工作的,那么你可以看看我寫的這篇文章從Haskell、JavaScript、Go看函數式編程。我們今天的目的是看看lodash是如何實現_.reduce的,以及和我們函數式的實現的區(qū)別。
我們看到lodash源代碼是這樣的:
function reduce(collection, iteratee, accumulator) { var func = isArray(collection) ? arrayReduce : baseReduce, initAccum = arguments.length < 3; return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach); }
在官方的注釋中說,對于對象,遍歷順序是無法保證的。我們不考慮這么復雜的情況,先看看Array的情況。其次,我們在調用_.reduce的時候沒有傳入第三個accumulator參數,那么函數可以簡化為:
function reduce(collection, iteratee, accumulator) { return arrayReduce(collection, getIteratee(iteratee, 4), accumulator, true, baseEach); }
在看看arrayReduce函數:
function arrayReduce(array, iteratee, accumulator, initAccum) { var index = -1, length = array == null ? 0 : array.length; if (initAccum && length) { accumulator = array[++index]; } while (++index < length) { accumulator = iteratee(accumulator, array[index], index, array); } return accumulator; }
這里的accumulator是初始累加值,如果傳入,則"折疊"在其基礎上進行,就上面的最簡單的例子而言,如果傳入第三個參數是2,那么返回值就會使8。
const _ = require("lodash"); _.reduce([1,2,3],function(a,b){return a+b},8); // 8
所以arrayReduce函數就是給定一個初始值然后進行迭代的函數。我們真正需要關注的函數式iteratee函數,即getIteratee(func, 4)這里的func就是我進行重命名之后的自定義函數。
這個getIteratee函數在介紹_.map的時候就進行介紹了,在func是一個function的情況下,就是返回func本身。
所以我們可以把整個reduce函數簡化為如下版本:
function reduce(array, func, accumulator) { var index = -1, length = array == null ? 0 : array.length; if (length) { accumulator = array[++index]; } while (++index < length) { accumulator = func(accumulator, array[index], index, array); } return accumulator; }
其實看上去很像一個”遞歸“函數,因為前面一次的運算結果將會用于下一次函數調用,但又不是遞歸函數。我們其實完全可以寫一個遞歸版本的reduce:
function reduce(array,func,accumulator){ accumulator = accumulator == null ? array[0]:accumulator; if (array.length >0){ var a = array.shift(); accumulator = func(a,accumulator); return reduce(array,func,accumulator); } return accumulator }
工作的也不錯,但在分析過程中,發(fā)現lodash一直在避免修改原參數的值,盡量讓整個函數調用時無副作用的。我覺得這個思想在開發(fā)過程中也有很多值得借鑒的地方。
_.curry了解過函數式編程的同學一定聽過大名鼎鼎的柯里化,在Lodash中也有一個專門用于柯里化的函數_.curry。這個函數接受一個函數func和這個函數的部分參數,然后返回一個接受剩余參數的函數func"。
我們看看這個函數是怎么實現的:
function curry(func, arity, guard) { arity = guard ? undefined : arity; var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); result.placeholder = curry.placeholder; return result; }
我們又看到我們的老朋友createWrap了,其實這個函數我們在上一篇文章中分析過,但是我們那時候是分析_.ary函數的時候進行了精簡,這次我們看看createWrap函數式怎么對_.curry函數進行處理的(將無關邏輯進行精簡):
function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { var isBindKey = 0 var length = 0; ary = undefined ; arity = arity === undefined ? arity : toInteger(arity); length -= holders ? holders.length : 0; 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 : nativeMax(newData[9] - length, 0); result = createCurry(func, bitmask, arity); var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); }
這里面的關鍵就是createCurry函數了:
function createCurry(func, bitmask, arity) { var Ctor = createCtor(func); function wrapper() { var length = arguments.length, args = Array(length), index = length, placeholder = getHolder(wrapper); while (index--) { args[index] = arguments[index]; } var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) ? [] : replaceHolders(args, placeholder); length -= holders.length; if (length < arity) { return createRecurry( func, bitmask, createHybrid, wrapper.placeholder, undefined, args, holders, undefined, undefined, arity - length); } var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; return apply(fn, this, args); } return wrapper; }
不得不說和createHybird函數十分相似,但是其中還有一個比較關鍵的函數,就是createRecurry,這個函數返回了一個能夠繼續(xù)進行curry的函數:
function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { var isCurry = bitmask & WRAP_CURRY_FLAG, newHolders = isCurry ? holders : undefined, newHoldersRight = isCurry ? undefined : holders, newPartials = isCurry ? partials : undefined, newPartialsRight = isCurry ? undefined : partials; bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG); bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG); if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) { bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG); } var newData = [ func, bitmask, thisArg, newPartials, newHolders, newPartialsRight, newHoldersRight, argPos, ary, arity ]; var result = wrapFunc.apply(undefined, newData); if (isLaziable(func)) { setData(result, newData); } result.placeholder = placeholder; return setWrapToString(result, func, bitmask); }
Lodash為了實現curry化,進行了多層的包裝,為了實現返回的是劃一的Lodash中定義的能夠curry化的函數。
這個函數要求接受相應的參數列表,即代碼中的data。在curry化的過程中有一個非常重要的東西,就是占位符placeholder。在對curry化的函數進行調用時也可以用占位符進行占位:
var curried = _.curry(abc); curried(1)(2)(3); // => [1, 2, 3] curried(1, 2)(3); // => [1, 2, 3] curried(1, 2, 3); // => [1, 2, 3] // Curried with placeholders. curried(1)(_, 3)(2); // => [1, 2, 3]
可以用下劃線_作為占位符占位。我們且不看lodash為我們做的很多復雜的預處理和特殊情況的處理,我們就分析_.curry函數實現的主要思想。首先_.curry函數有一個屬性存儲了最初的函數的接受函數參數的個數。然后有一個參數數組用于存儲部分參數,如果參數個數沒有滿足調用函數需要的個數,就繼續(xù)返回一個重新curry化的函數。
根據上面的思想我們可以寫出一個簡化的curry化代碼:
/** * *var abc = function(a, b, c) { * return [a, b, c]; *}; * *var curried = curry(abc); * *curried(1)(2)(3); * // => [1, 2, 3] * * curried(1, 2)(3); * // => [1, 2, 3] * * curried(1, 2, 3); * // => [1, 2, 3] * * // Curried with placeholders. * curried(1)("_", 3)(2) * 這就無法處理了 * // => [1, 3, 2] */ function curry(func){ function wrapper(){ func.prototype.that = func.prototype.that ? func.prototype.that : this; func.prototype.paramlength = func.prototype.paramlength ? func.prototype.paramlength: func.length ; func.prototype.paramindex = func.prototype.paramindex ?func.prototype.paramindex : 0; func.prototype.paramplaceholder = func.prototype.paramplaceholder ? func.prototype.paramplaceholder : Array(func.length); for (var i = 0 ; i < arguments.length; i++) { if (arguments[i] == "_"){ continue; }else{ func.prototype.paramplaceholder[func.prototype.paramindex] = arguments[i]; func.prototype.paramindex += 1; } } if (func.prototype.paramindex == func.prototype.paramlength){ func.prototype.paramindex = 0; return func.apply(func.prototype.that,func.prototype.paramplaceholder) } return wrapper; } return wrapper; }
我們雖然可以借助Lodash的思想實現我們一個簡單版本的curry函數,但是這個簡單版本的函數有一個問題,那就是,這個函數是借助閉包實現的,在整個執(zhí)行過程當中,只要被柯里化的函數沒有執(zhí)行結束,那么它就會一直存在在內存當中,它的一些屬性也會一直存在。第二個問題是,沒有辦法實現Lodash的"真正"的占位符,只是在遇到"_"的時候將其跳過了。
一個真正有效的柯里化函數實現起來有很多細節(jié)需要考慮,這就是Lodash存在的意義。我們應該在理解其實現原理的前提下,享受Lodash帶來的便利。
小結閱讀Lodash源碼真的能夠了解很多代碼實現上的細節(jié),Lodash在性能優(yōu)化上面做了很多工作,也給我們學習一個優(yōu)秀的js庫提供了非常好的參考。我在閱讀Lodash源代碼的過程中也會遇到很多不理解的地方。但是細細琢磨發(fā)其實它的代碼還是非常清晰易懂的。
待續(xù)下周將繼續(xù)更新Lodash源碼分析系列,接下來將會分析Lodash集合方法。
? 版權所有,禁止一切形式轉載。順便宣傳一下個人博客http://chenquan.me
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/85011.html
摘要:前言這是源碼分析系列文章的第三篇,前面兩篇文章源碼分析一源碼分析二分別分析了中的一些重要函數,也給出了簡化的實現,為理解其內部機理和執(zhí)行方式提供了便利。官方也對其進行了說明。 前言 這是Lodash源碼分析系列文章的第三篇,前面兩篇文章(Lodash 源碼分析(一)Function Methods、Lodash 源碼分析(二)Function Methods)分別分析了Lodash F...
摘要:依賴源碼分析之緩存使用方式的進一步封裝源碼分析之源碼分析之源碼分析之的實現源碼分析之源碼分析的調用如果有傳遞,則先調用,使用生成要比較數組的映射數組。循環(huán)完畢,沒有在第二個數組中發(fā)現相同的項時,將該項存入數組中。 外部世界那些破舊與貧困的樣子,可以使我內心世界得到平衡?!柧S諾《煙云》 本文為讀 lodash 源碼的第十七篇,后續(xù)文章會更新到這個倉庫中,歡迎 star:pocke...
摘要:作用與用法是的內部函數,之前在源碼分析之緩存介紹過一種這樣的數據結構這是一個二維數組,每項中的第一項作為緩存對象的,第二項為緩存的值。 這個世界需要一個特定的惡人,可以供人們指名道姓,千夫所指:全都怪你?!迳洗簶洹懂斘艺勁懿綍r我談些什么》 本文為讀 lodash 源碼的第六篇,后續(xù)文章會更新到這個倉庫中,歡迎 star:pocket-lodash gitbook也會同步倉庫的更新...
摘要:到這里,源碼分析完了。但是,有兩個致命的特性的遍歷不能保證順序會遍歷所有可枚舉屬性,包括繼承的屬性。的遍歷順序依賴于執(zhí)行環(huán)境,不同執(zhí)行環(huán)境的實現方式可能會不一樣。 小時候,鄉(xiāng)愁是一枚小小的郵票, 我在這頭, 母親在那頭。 長大后,鄉(xiāng)愁是一張窄窄的船票, 我在這頭, 新娘在那頭。 后來啊, 鄉(xiāng)愁是一方矮矮的墳墓, 我在外頭, 母親在里頭。 而現在, 鄉(xiāng)愁是一灣淺淺的海峽, 我在這頭, 大...
摘要:最簡單的案例以最簡單的情景為例在某一時刻點只調用一次函數,那么將在時間后才會真正觸發(fā)函數。后續(xù)我們會逐漸增加黑色鬧鐘出現的復雜度,不斷去分析紅色鬧鐘的位置。 序 相比網上教程中的 debounce 函數,lodash 中的 debounce 功能更為強大,相應的理解起來更為復雜; 解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼...
閱讀 886·2023-04-25 21:21
閱讀 3254·2021-11-24 09:39
閱讀 3102·2021-09-02 15:41
閱讀 2039·2021-08-26 14:13
閱讀 1856·2019-08-30 11:18
閱讀 2822·2019-08-29 16:25
閱讀 535·2019-08-28 18:27
閱讀 1614·2019-08-28 18:17