前端大白话之"库里"化(Currying)

591 阅读4分钟

前端大白话之"库里"化(Currying)

前期知识点:闭包,arguments,伪数组与数组的转换,合并数组。

啥是Currying?

5.jpg

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

Currying 的重要意义在于可以把函数完全变成接受一个参数,返回一个值的固定形式,函数curry之后,可以只提供一个参数,其他的参数作为这个函数的“环境”来创建。这就能让函数回归到原始的一个参数进去一个值出来的状态,它既能减少代码冗余,也能增加可读性,这样对于讨论和优化也会更加方便。

Currying(柯里化)是如何工作的?

它的工作方式是通过为每个可能的参数嵌套函数,使用由嵌套函数创建的自然闭包来保留对每个连续参数的访问。

大白话

日常看完概念不懂啥意思?

中文中XX化就是一个事物的变化,比如美化、丑化、情绪化、复杂化。

柯里化简单来说首先它是一个对函数(方法)的包装。

包装成什么样子呢?

把多个参数的原函数包装成可以接受一个参数的样子。举个栗子:

function addition(a, b) {
    return a + b;
}
addition(1, 2); // 3
// 让其支持
addition(1)(2); // 3

观察addition(1)(2),凭什么这个函数可以连续调用呢?说明addition(1)的返回值还是一个函数。

所以就有了这样的实现,话不多说,先上代码。

function currying(fn, ...args) {
    // 如果参数个数小于最初的 fn.length,则递归调用,继续收集参数
    // 这里 fn.length是指声明函数的参数的个数
    if (args.length < fn.length) {
        return (...newArgs) => currying(fn, ...args, ...newArgs);
    } else {
        return fn(...args);
    }
}
function addition(a, b, c) {
    return a + b + c;
}
let sum = currying(addition);
sum(3);// 原函数 addition中声明了a b c三个参数,这里我们只传了一个参数3,所以args.length < fn.length,返回值还是一个函数
sum(1,2)(3,4);// 6 这里我们先传了两个参数1 2,所以sum(1,2)返回值依然是一个函数。紧接着再次调用传入3 4,一共传了4个参数。所以args.length > fn.length,所以返回fn(...args)也就是addition(1,2,3,4)

进阶

机智的我很快就不能满足于此,因为我发现这样不能一直调用,因为当传入参数的个数比原函数声明的参数多的时候,返回的不再是一个函数,所以不能连续一直调用。先来看看我写的这个函数:

function addition() {
    if (!arguments.length) return;
    return [...arguments].reduce((a, b) => a + b);
}

这个函数声明的参数甚至是0。我想写一个currying函数让其可以currying(addition)(1)(2)(3)(4,5)(6,7,8)(9)随意调用,这样多爽!

先看一道面试题:

//实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) = 6;
// add(1, 2, 3)(4) = 10;
// add(1)(2)(3)(4)(5) = 15;

直接上解法

function add(...args) {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    let _args = args || [];
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    let _adder = function (...newArgs) {
        _args = _args.concat(newArgs); //_args = [..._args,...newArgs]
        return _adder;
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce((a, b) => a + b);
    };
    return _adder;
}

// console.log(add(1)(2, 4)); // f 7
// console.log(add(1)(2)(3)); // f 6
// console.log(add(1, 2, 3)(4)); // f 10
// console.log(add(1)(2)(3)(4)(5)); // f 15
// console.log(add(2, 6)(1)); // f 9
// console.log(typeof add(1)(2)); // function
// console.log(add(1)(2).toString()); // 3 <number>
// console.log(String(add(1)(2))); // 3 <string>
// console.log(Number(add(1)(2))); // 3 <number>
// console.log(add(1)(2) - 0); // 3 <number>

由此引申出一个通用的currying

function currying(fn, ...args1) {
    // 若未传入fn则return
    if (!fn) return;
    const that = this;
    // 定义一个数组存储每次传入的参数
    let args = args1 || [];
    // 定义一个函数 foo,并最后返回这个函数 foo,使 currying 后的函数可以继续调用。
    let foo = function (...args2) {
        // 通过 currying 后函数传入的参数来判断是让其继续返回函数还是返回结果(值)
        if (args2.length === 0) {
            // 若currying 后的函数未传参,比如 currying(addition)()或者currying(addition)(1)(),则返回结果
            return fn.call(that, ...args);
        } else {
            // 否则继续返回此函数让其继续收集参数
            args = [...args, ...args2];
            return foo;
        }
    };
    // 最后返回定义的函数 foo
    return foo;
}

function addition() {
    if (!arguments.length) return;
    return [...arguments].reduce((a, b) => a + b);
}
function subtraction(a, b) {
    return a - b;
}
function multiplication(a, b) {
    return a * b;
}
function division(a, b) {
    return a / b;
}
console.log(currying(addition)(1)()); // 1
console.log(currying(addition)(2)(3)()); // 5
console.log(currying(multiplication, 5)(4)()); // 20

柯里化的作用

  • 参数复用
  • 提前返回,提高适用性
  • 延迟执行——函数的主体本身不执行,可以看成是延迟执行

Function.prototype.bind 方法也是柯里化应用。