lodash 源码解析 -- curry,函数式编程利器

1,931 阅读5分钟

前言

柯里化是函数式编程中不可或缺的一环,这个函数使得接受多参数的函数可以通过不影响结果的方式使其成为接受单参数的函数及其序列,根据这个特点可以合成出很多类似功能的函数,甚至是以自定义的顺序运行代码,达到复用代码的目的

lodash 中的 curry 函数因为处理了很多边界情况,这里有些我也不是很清楚。因此这次我只会分析基础的 curry 相关的代码,其余的部分不会详细说明,有一些不足欢迎在评论里补充

思路分析

1. 流程图

About curry.png

2. 简易的 curry 函数

这里贴一个自己实现的简易的 curry 函数,以便理清思路。可以看到柯里化最主要的部分就是对函数进行包裹,在这个包裹内把参数存储起来,等到参数的数量足够再执行

function curry(func, ...args1) {
    return function wrapper (...args2) {
        if (args1.length + args2.length >= func.length) {
            return func.apply(null, [...args1, ...args2])
        }
        return curry(func, ...args1, ...args2)
    }
}

源码分析

1. curry

1. 传入参数
  • func 函数
  • arity 传入函数的参数
  • guard 可以让 curry 成为可迭代的函数,用于 _.map 类似的函数
2. 源码分析

curry 上的 placeholder 传入到 result 上,让 result 也可以使用 placeholder。这里的 placehoder 是 lodash 在外部手动设置的,可以通过传入 lodash 本身的变量,即 _ 作为占位符

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;
}

//...
arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) {
  lodash[methodName].placeholder = lodash;
});
//...

2. createWrap

1. 传入参数
  • func 传入的被柯里化的函数
  • bitmask 标志位
  • thisArg 传递 this 指向
  • partials 已经包含的参数
  • holders 占位符
  • argPos 参数的位置,re-arg 函数会用到
  • ary 参数接收的数量,ary 函数会用到
  • arity 后传入的参数
2. 源码分析
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);
}

检测 createWrap 是否是用于 bind 绑定函数

  var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;
  if (!isBindKey && typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }

partialsundefinedlength 置为 0

var length = partials ? partials.length : 0;

如果 length0,把 partialpartialRight 功能也关闭,同时 holderspartials 置为 0aryarity 和传入时保持一致

if (!length) {
  bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);
  partials = holders = undefined;
}

ary 不变,arity 不变,length 置为 0(根据 holders 占位符的数量决定)

ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);
arity = arity === undefined ? arity : toInteger(arity);
length -= holders ? holders.length : 0;

检测 是否使用 partial 的功能

if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {
  var partialsRight = partials,
      holdersRight = holders;
  partials = holders = undefined;
}

data 置为 弱引用 metaMapfuncvalue

var data = isBindKey ? undefined : getData(func);

初始化 newDatanewData 包含了被柯里化的函数,标志位,this 指针,绑定的参数,占位符,绑定的右起的参数,右起的占位符

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);

根据 bitmask 使用对应的功能,这里使用的是 WRAP_CURRY_FLAG

  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;

设置 toString 方法,对返回的函数的 toString 方法进行设置,设置为返回 原函数的 + 带有 /* [wrapped with _.curry] */ 标识的字符串

  return setWrapToString(setter(result, newData), func, bitmask);

3. creatCurry

1. 传入参数
  • func 传入的被柯里化的函数
  • bitmask 标志位
  • arity 传入给 createCurry 的需要绑定的参数
2. 源码分析

过滤 placeholder,同时返回柯里化的函数

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;
}

提取 func 作为构造函数,如果柯里化的是构造函数,也可以保证其正常接收参数并返回

  var Ctor = createCtor(func);

初始化,获取 placeholder,这里的 placeholder 可以是 lodash 默认的,也可以是在 wrapper 上设置的

var length = arguments.length,
    args = Array(length),
    index = length,
    placeholder = getHolder(wrapper);

如果传入参数小于函数参数 arity,调用 createRecurry,返回新的函数

if (length < arity) {
  return createRecurry(
    func, bitmask, createHybrid, wrapper.placeholder, undefined,
    args, holders, undefined, undefined, arity - length);
}

参数大于等于 arity ,传入参数返回函数调用结果,这里检测是否是构造函数,只要参数足够,柯里化的函数也可以作为构造函数使用,如果不是构造函数就正常返回调用值

var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
    return apply(fn, this, args);

4. createRecurry

1. 传入参数

类似 creatCurry

2. 源码分析

wrapper 函数会调用 createRecurry 最终处理绑定的参数和 懒加载 等情况,然后交给 createHybrid 返回新函数或是返回结果

function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
  // 根据 bitmask 和 isCurry 标志位,设置 holders 、 partials 等参数
  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);
  // 清除 bind 标志位
  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
  ];
    // 调用 createHybird,借用这个函数的能力返回一个新函数
  var result = wrapFunc.apply(undefined, newData);
  if (isLaziable(func)) {
    setData(result, newData);
  }
  // 设置 placeholder
  result.placeholder = placeholder;
  // 同样的,将 toString 设置为返回 原函数的 + 带有 /* [wrapped with _.curry] */ 标识的字符串
  return setWrapToString(result, func, bitmask);
}

5. createHybrid

1. 传入参数

类似 createCurry

2. 源码分析

主要作用就是返回 wrapper ,类似于 createCurry,返回一个 wrapper 函数

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;
}

这里的 wrapper 其实类似上面的 createCurry,增加了绑定之前参数的代码

  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 (isAry && ary < length) {
      args.length = ary;
    }
    if (this && this !== root && this instanceof wrapper) {
      fn = Ctor || createCtor(fn);
    }
    return fn.apply(thisBinding, args);
  }

应用场景

curry 函数可以包裹现有的函数,用于复用函数,比如一个 add 函数,可以通过柯里化的方式包裹,形成了新的函数

const add =_.curry(add)
let add10 = add(10)
let add100 = add(100)

总结

柯里化的实现,用到了 JS 的闭包特性,存储了传入的参数,闭包通常指的是自带执行环境的函数。

柯里化虽然提升了代码的复用率,但也有问题,比如对执行逻辑的复杂化,比如在内存中产生了很多闭包