ToC
高阶函数
高阶函数并不是 JS 独有的概念,在其他语言中也能发现它的影子。高阶函数的核心概念就是将函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。举个详细的例子:
1 function test() {} // 函数可以直接声明2 var test1 = function() {} // 函数可以赋值给变量3 // 函数参数接收变量4 function test2(arg) {}5 // 那么函数、也可以接受变量作为参数6 test2(test1)
JS 中内置的高阶函数有很多,比如 setTimeout
setInterval
[].map()
[].reduce()
[].filter
,这里引申出一个问题,高阶函数的好处是什么?
实际上高阶函数最大的好处是抽象函数,便于扩展。将函数由相互依赖的程序体抽象成完全独立的函数体。
函数柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
它在实际使用效果是这样的:
1function curry(fn) {}2
3function add(a,b,c,d) {4 return a + b + c + d5}6
7// 正常调用 add8add(1,2,3,4)9
10// 使用柯里化16 collapsed lines
11var curryAdd = curry(add)12curryAdd(1) // 参数个数不足,返回一个新的函数13curryAdd(2) // 参数个数不足,返回一个新的函数14curryAdd(3) // 参数个数不足,返回一个新的函数15curryAdd(4) // 返回最终的结果16
17// 还可以第一次就把前三个参数传好18var curryAdd = curry(add, 1, 2, 3)19curryAdd(4) // 返回最终结果20
21// 或者时不定参数个数22var curryAdd = curry(add)23var curryAdd2 = curryAdd(1, 2)24var curryAdd3 = curryAdd2(3)25
26curryAdd3(4) // 返回最终结果
初看这种效果好像没啥用,但是仔细想想,如果一个函数有多个参数,而我前几个参数的值是固定的,已经确认好结果的,如果不用柯里化函数,那每次调用的时候都需要传入完成的参数,但是如果使用了柯里化函数,并且将那些已确定的参数传入,那最终在调用的时候,就只需要传入每次需要变化的那个参数即可,这才是柯里化函数真正的用途。
curry 实现
柯里化的基本实现可以简化成以下版本的代码:
1// 第一个函数表示需要将哪个函数转换为柯里化函数2// 从第二个参数开始,往后所有的参数都是传递给第一个函数参数的3function curry(fn, ...args) {4 // 返回一个新的函数,接收剩下的函数5 return function (..._args) {6 const params = args.concat(_args)7
8 return fn.apply(this, params)9 }10}4 collapsed lines
11
12// 使用13var curryAdd = curry(add, 1, 2)14console.log(curryAdd(3, 4)) // 10
但这只是实现了最基本的功能,因为它还没有对参数个数进行判断,柯里化函数的特点就是在参数个数不满足函数要求之前返回的东西都是一个函数,而只有满足了参数个数之后,得到的才是返回的结果。以下是改良版:
1function curry(fn, ...args) {2 // 如果第一次执行时参数个数足够则直接函数函数调用的结果3 if (args.length >= fn.length) {4 return () => fn(...args)5 }6
7 // 如果参数不够则返回一个新的函数,将上次执行和下一次执行的参数收集起来8 return (...params) => curry(fn, [...args, ...params])9}
1// 测试一下2function add(a, b, c, d) {3 return a + b + c + d4}5
6console.log(curry(add)) // [Function (anonymous)]7console.log(curry(add, 1)) // [Function (anonymous)]8console.log(curry(add, 1, 2)) // [Function (anonymous)]9console.log(curry(add, 1, 2, 3)) // [Function (anonymous)]10console.log(curry(add, 1, 2, 3, 4)()) // 10(第一次就把所有参数给设置好,但依然需要返回一个函数,因为 curry 的本质上就是返回一个接收任意参数个数的函数)1 collapsed line
11console.log(curry(add)(1)(2)(3)(4)) // 10
偏函数
在了解偏函数之前,我们先了解一下什么是函数的元。有一个函数有两个参数,那么它就叫二元函数。这里的元就表示参数的个数。而减少函数的个数就可以叫做降元。
在计算机科学中,偏函数叫做部分应用、局部应用。指固定的一个函数的一些参数,然后产生另一个更小元的函数。
1function add(a, b, c) {2 return a + b + c3}4
5var add1 = partial(add, 1)6
7add1(2, 3)
是不是似曾相识?它好像和那个柯里化有点相似啊
与柯里化有什么区别?
偏函数与柯里化在语法上看不出什么区别,但他们的目的是不一样的。
柯里化的目的是返回一个接收任意参数个数的函数来实现最终被包装函数的调用,而偏函数则是为了简化函数参数,固定函数参数中的部分参数,使得最终调用时更简单,像这样:
1// 比如说有一个函数,它会做很多事情,同时也包括将传入的参数转换成对应进制的值2function parseNumber(value, radix) {3 // ...do something4 const number = parseInt(value, radix)5}6
7// 正常情况下调用:8parseNumber('1', 10)9parseNumber('3', 10)10parseNumber('10', 10)6 collapsed lines
11
12// 而使用偏函数包装以后13const newParseNumber = partial(parseNumber, 10)14newParseNumber('1')15newParseNumber('3')16newParseNumber('10')
而其实最简单的偏函数使用一个 bind 就能实现:
1function add(a, b, c, d) {2 return a + b + c + d3}4
5const newAdd = add.bind(null, 1, 2)6console.log(newAdd(3, 4))
具体实现
以上确实能实现,但是它使用需要传入第一个参数作为指向,那我们可以在 Function.prototype 上增加一个方法,替我们去做这件事
1Function.prototype.partial = function(...args) {2 const _self = this3
4 return function (...params) {5 return _self.apply(this, [...args, ...params])6 }7}8
9const newAdd = add.partial(1, 2)10
1 collapsed line
11newAdd(3, 4) // 10
我们可以试试用这个来实现一个实例
1function formatSentence(time, opt) {2 return time + ' ' + opt.user_class + ' ' + opt.name + ': ' + opt.sentence3}4
5const outPutSentence = formatSentence.partial(new Date().getHours() + ':' + new Date().getMinutes())6
7console.log(outPutSentence({8 user_class: '管理员',9 name: '测试',10 sentence: '欢迎大家'1 collapsed line
11}))
以上。