这是Lodash源码分析的第二篇文章,咱们在第一篇Lodash 源码分析(一)“Function” Methods中介绍了基本的_.after
,_.map
,以及复杂的_.ary
函数的实现以及咱们本身的自定义轻量级版本。大概清楚了Lodash的整个代码脉络。此次咱们继续分析,此次咱们讲讲_.reduce
和_.curry
。javascript
_.reduce
我一直以为,若是可以理解_.map
和_.reduce
的实现,那么任何复杂的函数都不在话下。咱们已经介绍了_.map
的实现,知道了_.map
函数中是如何处理集合,并将其逐个进行函数处理的。咱们知道在_.map
函数中会把三个参数传到给定的函数中,分别是array[index]
,index
和array
。此次咱们看看_.reduce
函数。java
众所周知,_.reduce
函数可以将一个集合进行"折叠"。"折叠"理解起来比较抽象。咱们能够经过代码做为样例说明一下:编程
const _ = require("lodash"); _.reduce([1,2,3],function(a,b){return a+b}); // 6
若是你不知道_.reduce
究竟是怎么工做的,那么你能够看看我写的这篇文章从Haskell、JavaScript、Go看函数式编程。咱们今天的目的是看看lodash是如何实现_.reduce
的,以及和咱们函数式的实现的区别。segmentfault
咱们看到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
。app
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 }
工做的也不错,但在分析过程当中,发现lodash一直在避免修改原参数的值,尽可能让整个函数调用时无反作用的。我以为这个思想在开发过程当中也有不少值得借鉴的地方。
_.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
,这个函数返回了一个可以继续进行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
函数有一个属性存储了最初的函数的接受函数参数的个数。而后有一个参数数组用于存储部分参数,若是参数个数没有知足调用函数须要的个数,就继续返回一个从新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
函数,可是这个简单版本的函数有一个问题,那就是,这个函数是借助闭包实现的,在整个执行过程中,只要被柯里化的函数没有执行结束,那么它就会一直存在在内存当中,它的一些属性也会一直存在。第二个问题是,没有办法实现Lodash的"真正"的占位符,只是在遇到"_"的时候将其跳过了。
一个真正有效的柯里化函数实现起来有不少细节须要考虑,这就是Lodash存在的意义。咱们应该在理解其实现原理的前提下,享受Lodash带来的便利。
阅读Lodash源码真的可以了解不少代码实现上的细节,Lodash在性能优化上面作了不少工做,也给咱们学习一个优秀的js库提供了很是好的参考。我在阅读Lodash源代码的过程当中也会遇到不少不理解的地方。可是细细琢磨发其实它的代码仍是很是清晰易懂的。
下周将继续更新Lodash源码分析系列,接下来将会分析Lodash集合方法。
© 版权全部,禁止一切形式转载。顺便宣传一下我的博客http://chenquan.me