「前端面试题系列6」理解函数的柯里化

栏目: JavaScript · 发布时间: 5年前

内容简介:这是前端面试题系列的第 6 篇,你可能错过了前面的篇章,可以在这里找到:最近,朋友T 在准备面试,他为一道编程题所困,向我求助。原题如下:

「前端面试题系列6」理解函数的柯里化

前言

这是前端面试题系列的第 6 篇,你可能错过了前面的篇章,可以在这里找到:

最近,朋友T 在准备面试,他为一道编程题所困,向我求助。原题如下:

// 写一个 sum 方法,当使用下面的语法调用时,能正常工作
console.log(sum(2, 3)); // Outputs 5
console.log(sum(2)(3)); // Outputs 5

这道题要考察的,就是对函数柯里化的理解。让我们先来解析一下题目的要求:

  • 如果传递两个参数,我们只需将它们相加并返回。
  • 否则,我们假设它是以sum(2)(3)的形式被调用的,所以我们返回一个匿名函数,它将传递给sum()(在本例中为2)的参数和传递给匿名函数的参数(在本例中为3)。

所以,sum 函数可以这样写:

function sum (x) {
    if (arguments.length == 2) {
        return arguments[0] + arguments[1];
    }
    
    return function(y) {
        return x + y;
    }
}

arguments 的用法挺灵活的,在这里它则用于分割两种不同的情况。当参数只有一个的时候,进行柯里化的处理。

那么,到底什么是函数的柯里化呢?接下来,我们将从概念出发,探究函数柯里化的实现与用途。

什么是柯里化

柯里化,是函数式编程的一个重要概念。它既能减少代码冗余,也能增加可读性。另外,附带着还能用来装逼。

先给出柯里化的定义:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

柯里化的定义,理解起来有点费劲。为了更好地理解,先看下面这个例子:

function sum (a, b, c) {
    console.log(a + b + c);
}
sum(1, 2, 3); // 6

毫无疑问,sum 是个简单的累加函数,接受3个参数,输出累加的结果。

假设有这样的需求,sum的前2个参数保持不变,最后一个参数可以随意。那么就会想到,在函数内,是否可以把前2个参数的相加过程,给抽离出来,因为参数都是相同的,没必要每次都做运算。

如果先不管函数内的具体实现,调用的写法可以是这样: sum(1, 2)(3); 或这样 sum(1, 2)(10); 。就是,先把前2个参数的运算结果拿到后,再与第3个参数相加。

这其实就是函数柯里化的简单应用。

柯里化的实现

sum(1, 2)(3); 这样的写法,并不常见。拆开来看, sum(1, 2) 返回的应该还是个函数,因为后面还有 (3) 需要执行。

那么反过来,从最后一个参数,从右往左看,它的左侧必然是一个函数。以此类推,如果前面有n个(),那就是有n个函数返回了结果,只是返回的结果,还是一个函数。是不是有点递归的意思?

网上有一些不同的柯里化的实现方式,以下是个人觉得最容易理解的写法:

function curry (fn, currArgs) {
    return function() {
        let args = [].slice.call(arguments);

        // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }

        // 递归调用
        if (args.length < fn.length) {
            return curry(fn, args);
        }

        // 递归出口
        return fn.apply(null, args);
    }
}

解析一下 curry 函数的写法:

首先,它有 2 个参数,fn 指的就是本文一开始的源处理函数 sum 。currArgs 是调用 curry 时传入的参数列表,比如 (1, 2)(3) 这样的。

再看到 curry 函数内部,它会整个返回一个匿名函数。

再接下来的 let args = [].slice.call(arguments); ,意思是将 arguments 数组化。arguments 是一个类数组的结构,它并不是一个真的数组,所以没法使用数组的方法。我们用了 call 的方法,就能愉快地对 args 使用数组的原生方法了。在这篇 「干货」细说 call、apply 以及 bind 的区别和用法 中,有关于 call 更详细的用法介绍。

currArgs !== undefined 的判断,是为了解决递归调用时的参数拼接。

最后,判断 args 的个数,是否与 fn (也就是 sum )的参数个数相等,相等了就可以把参数都传给 fn,进行输出;否则,继续递归调用,直到两者相等。

测试一下:

function sum(a, b, c) {
    console.log(a + b + c);
}

const fn = curry(sum);

fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
fn(1)(2, 3); // 6
fn(1)(2)(3); // 6

都能输出 6 了,搞定!

柯里化的用途

理解了柯里化的实现之后,让我们来看一下它的实际应用。柯里化的目的是,减少代码冗余,以及增加代码的可读性。来看下面这个例子:

const persons = [
    { name: 'kevin', age: 4 },
    { name: 'bob', age: 5 }
];

// 这里的 curry 函数,之前已实现
let getProp = curry(function (key, obj) {
    return obj[key];
});

const ages = persons.map(getProp('age')); // [4, 5]
const names = persons.map(getProp('name')); // ['kevin', 'bob']

在实际的业务中,我们常会遇到类似的列表数据。用 getProp 就可以很方便地,取出列表中某个 key 对应的值。

另外,为了便于理解调用的写法,可以扩展一下:

const names = persons.map(getProp('name'));

等价于:

const names = persons.map(item => {
    return getProp('name', item);
});

最后,来看一个 Memoization 的例子。它用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。

function memoizeFunction(func) {
    const cache = {};
    return function() {
        let key = arguments[0];
        if (cache[key]) {
            return cache[key];
        } else {
            const val = func.apply(null, arguments);
            cache[key] = val;
            return val;
        }
    };
}

const fibonacci = memoizeFunction(function(n) {
    return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(100)); // 输出354224848179262000000
console.log(fibonacci(100)); // 输出354224848179262000000

代码中,第2次计算 fibonacci(100) 则只需要在内存中直接读取结果。

总结

函数的柯里化,是 Javascript 中函数式编程的一个重要概念。它返回的,是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。

虽然一开始理解起来有点云里雾里的,但一旦理解了其中的含义和具体的使用场景,用起来就会得心应手了。

PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。

「前端面试题系列6」理解函数的柯里化


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

无懈可击的Web设计

无懈可击的Web设计

【美】Dan Cederholm / 马跃 / 清华大学出版社 / 2012-5 / 39.00元

本书将指导您采用标准设计策略来满足以各种方式浏览网页的各类用户的需要。每章首先列举一个沿用传统HTML技术的实例,然后指出该实例的局限性,并利用XHTML和CSS对其进行重构。从中您将学会如何用简洁高效的HTML标记和CSS来取代臃肿的代码,从而创建加载速度极快、能供所有用户使用的网站。本书最后将前面各章讨论的所有页面组件珠联璧合地结合在一起,制作了一个页面模板。这一版全面润色和更新了上一版本,介......一起来看看 《无懈可击的Web设计》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码