这是前端面试题系列的第 6 篇,你可能错过了前面的篇章,能够在这里找到:前端
最近,朋友T 在准备面试,他为一道编程题所困,向我求助。原题以下:面试
// 写一个 sum 方法,当使用下面的语法调用时,能正常工做 console.log(sum(2, 3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5
这道题要考察的,就是对函数柯里化的理解。让咱们先来解析一下题目的要求:编程
因此,sum 函数能够这样写:segmentfault
function sum (x) { if (arguments.length == 2) { return arguments[0] + arguments[1]; } return function(y) { return x + y; } }
arguments 的用法挺灵活的,在这里它则用于分割两种不一样的状况。当参数只有一个的时候,进行柯里化的处理。数组
那么,到底什么是函数的柯里化呢?接下来,咱们将从概念出发,探究函数柯里化的实现与用途。缓存
柯里化,是函数式编程的一个重要概念。它既能减小代码冗余,也能增长可读性。另外,附带着还能用来装逼。app
先给出柯里化的定义:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。函数式编程
柯里化的定义,理解起来有点费劲。为了更好地理解,先看下面这个例子:函数
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 函数,以前已实现 const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; }); const ages = persons.map(getProp('age')); // [4, 5] const names = persons.map(getProp('name')); // ['kevin', 'bob']
在实际的业务中,咱们常会遇到相似的列表数据。用 getProp 就能够很方便地,取出列表中某个 key 对应的值。
须要注意的是,const names = persons.map(getProp('name'));
执行这条语句时 getProp 的参数只有一个 name
,而定义 getProp 方法时,传入 curry 的参数有2个,obj
和 index
(这里必须写 2 个及以上的参数)。
为何要这么写?关键就在于 arguments
的隐式传参。
const getProp = curry(function (obj, index) { console.log(arguments); // 会输出4个类数组,取其中一个来看 // { // 0: {name: "kevin", age: 4}, // 1: 0, // 2: [ // {name: "kevin", age: 4}, // {name: "bob", age: 5} // ], // 3: "age" // } });
map 是 Array 的原生方法,它的用法以下:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]);
因此,咱们传入的 name
,就排在了 arguments 的最后。为了拿到 name
对应的值,须要对类数组 arguments 作点转换,让它可使用 Array 的原生方法。因此,最终 getProp 方法定义成了这样:
const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; });
固然,还有另一种写法,curry 的实现更好理解,可是调用的代码却变多了,你们能够根据实际状况进行取舍。
const getProp = curry(function (key, obj) { return obj[key]; }); const ages = persons.map(item => { return getProp(item)('age'); }); const names = persons.map(item => { return getProp(item)('name'); });
最后,来看一个 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:欢迎关注个人公众号 “超哥前端小栈”,交流更多的想法与技术。