函数柯里化

一、函数柯里化 [curring](闭包应用)

  1. 核心:预先处理 / 预先存储
  2. 是什么:凡是形成一个闭包存储一些信息,供其下级上下文调取使用的,都是柯理化思想」

举例小题:

  1. const fn = (...params) => {
  2. // params->[1,2]
  3. return (...args) => {
  4. // args->[3]
  5. // 参数拼接
  6. return params.concat(args).reduce((total, item) => {
  7. return total + item;
  8. });
  9. };
  10. };
  11. let total = fn(1, 2)(3);
  12. console.log(total);
  • alert 和console 都会把 fn 转换为 String 处理
  1. function fn() {
  2. // ...
  3. let total = 10;
  4. return total;
  5. }
  6. // toString/valueOf/[symbol.toPrimitive]
  7. fn[Symbol.toPrimitive] = function () {
  8. return 10;
  9. };
  10. fn.toString = function () {
  11. return 'OK';
  12. }
  13. // alert(fn); //会把fn变为String然后再输出
  14. // console.log(fn); //基于console.log也会把fn转换为String处理,只不过和alert输出的结果看起来不一样而已

二、大厂面试:

  1. 利用函数柯里化,保存值
  2. 知道console.log调用toString方法
  3. 分析多次函数调用,都要返回什么
  1. const curring = () => {
  2. let arr = [];
  3. const add = (...params) => {
  4. arr = arr.concat(params); // 保存值(利用闭包不被销毁)
  5. return add; // 执行add的时候还是返回add 后面连续调用
  6. };
  7. add.toString = () => { // console的时候会调用该方法
  8. return arr.reduce((total, item) => {
  9. return total + item;
  10. });
  11. };
  12. return add; // 执行curring返回add 继续使用
  13. };
  14. //
  15. let add = curring(); // 柯里化(curring)
  16. let res = add(1)(2)(3);
  17. console.log(res); //->f 6
  18. add = curring();
  19. res = add(1, 2, 3)(4);
  20. console.log(res); //->f 10
  21. add = curring();
  22. res = add(1)(2)(3)(4)(5);
  23. console.log(res); //->f 15
  24. /*************传一个次数*************/
  25. const curring = n => {
  26. let arr = [],
  27. index = 0;
  28. const add = (...params) => {
  29. index++;
  30. arr = arr.concat(params);
  31. if (index >= n) {
  32. return arr.reduce((total, item) => {
  33. return total + item;
  34. });
  35. }
  36. return add;
  37. };
  38. return add;
  39. };
  40. let add = curring(5);
  41. res = add(1)(2)(3)(4)(5);
  42. console.log(res);

curry与偏函数

curry

function currying 把接受多个参数的函数转换成接受一个单一参数的函数

  1. // 非函数柯里化
  2. var add = function (x,y) {
  3. return x+y;
  4. }
  5. add(3,4) //7
  6. // 函数柯里化
  7. var add2 = function (x) {
  8. //**返回函数**
  9. return function (y) {
  10. return x+y;
  11. }
  12. }
  13. add2(3)(4) //7

在上面的例子中,我们将多维参数的函数拆分,先接受第一个函数,然后返回一个新函数,用于接收后续参数。

就此,我们得出一个初步的结论:*_柯里化后的函数_,如果形参个数等于实参个数,返回函数执行结果,否者,返回一个柯里化函数。

通过柯里化可实现代码复用,使用函数式编程。

实现柯里化函数

从上面例子中,我们定义了有两个形参的函数,为了实现柯里化,函数传入第一个形参后返回一个函数用来接收第二个形参。那么如果我们的定义的形参有个,那么也就需要嵌套2层,分别处理后两个参数,如

  1. var add3 = function (x) {
  2. return function (y) {
  3. return function (z) {
  4. return x + y + z;
  5. }
  6. }
  7. }
  8. add3(1)(3)(5)

如果形参有5个,7个呢?这里我们使用递归,进行简化。不知有没有看到规律,形参的个数决定了函数的嵌套层数。 即 有n个参数就得嵌套n-1个函数 ,那我们来改造一番。

  1. // 通用型柯里化
  2. function currying (fn) {
  3. // 未柯里化函数所需的参数个数 https://www.cnblogs.com/go4it/p/9678028.html
  4. var limit = fn.length
  5. var params = [] // 存储递归过程的所有参数,用于递归出口计算值
  6. return function _curry (...args) {
  7. params = params.concat(...args) // 收集递归参数
  8. if (limit <= params.length) {
  9. let tempParams=params.slice(0,limit)
  10. if(limit===params.length){ //参数个数满足时清除已缓存的参数
  11. params=[]
  12. }
  13. // 返回函数执行结果
  14. return fn.apply(null, tempParams)
  15. } else {
  16. // 返回一个柯里化函数
  17. return _curry
  18. }
  19. }
  20. }
  21. function add(x,y,z){
  22. return x + y+z;
  23. }
  24. // 函数柯里化
  25. var addCurried=currying(add);
  26. console.log(`addCurried(1)(2)(3)`,addCurried(1)(2)(3))//6
  27. console.log(`addCurried(3,3,3)`,addCurried(3,3,3))//9
  28. console.log(`addCurried(1,2)(3)`,addCurried(1,2)(3))//6
  29. console.log(`addCurried(3)(4,5)`,addCurried(3)(4,5))//12

我们看看addCurried(1)(2)(3)中发生了什么:

  1. 首先调用`addCurried(1),将1保存在词法环境中,然后递归调用_curry继续收集后续参数
  2. addCurried(1)(2),参数2与第一次的参数1,合并调用,因未达到形参个数要求,继续递归返回_curry
  3. 调用addCurried(1)(2)(3),参数为3,在接下去的调用中,与1,2进行合并,传入原函数add

注意点

  1. 柯里化基于闭包实现,可能会导致内存泄露
  2. 使用递归,执行会降低性能,递归多时会发生栈溢出,需要进行递归优化,参考
  3. arguments是类数组,使用Array.prototype.slice.call转换为数组时,效率低。

偏函数

简单描述,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单。

  1. // 乘法
  2. let multi = (x,y) => x * y;
  3. // 构造一个对数值乘以2的函数
  4. let double = multi.bind(null,2);
  5. console.log(double(3));//6
  6. console.log(double(5));//10

在这个例子中,我们使用bind 固定了 乘数,返回一个函数。该函数接受一个参数作为 被乘数。—将部分参数固定,只对剩余参数进行计算。

基于以上推导,我们来实现一个无绑定上下文的偏函数:

  1. /**
  2. * 偏函数实现
  3. * @param func 应用函数
  4. * @param argsBound 固定参数
  5. * @return {function(...[*]): *}
  6. */
  7. let partial = (func, ...argsBound) => {
  8. if (typeof func !== 'function') throw new TypeError(
  9. `${typeof func} is not a function`)
  10. return function (...args) { // (*)
  11. if(func.length-argsBound.length>args.length) throw new Error(`miss arguments`)
  12. return func.call(this, ...argsBound.concat(...args))
  13. }
  14. }
  15. let partialMulti= partial(multi,2)
  16. console.log(partialMulti());//Error: miss arguments
  17. console.log(partialMulti(3));//6

partial(func[, arg1, arg2...]) 调用的结果是一个基于 func 的封装函数,以及:

  • 和它传入的函数一致的 this
  • 然后传入 ...argsBound —— 来自偏函数调用传入的参数
  • 然后传入 ...args —— 传入封装函数的参数

区别

偏函数与柯里化很相似,下面我们做个对比:

柯里化:将一个对参数函数转换成多个单参数的函数,也就是将一个n元函数转换为n个一元函数。

偏函数:固定一个函数的一个或多个参数,也就是将一个n元函数转换成一个n-x元函数。

个人理解:偏函数是柯里化的一种特定的应用场景

使用场景

  • 动态生成函数
  • 减少参数
  • 延迟计