函数柯里化
柯里化的定义
红宝书(第三版): 用于创建已经设置好了一个或多个参数的函数。基本使用方法是使用一个闭包返回一个函数。(P604)
维基百科:柯里化(英语:Currying), 是把接受多个参数的函数变换成接受一个单元参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。(原链接)
官方解释看得有点懵,大白话概述一下:
柯里化技术,主要体现在函数里返回函数。 就是将多变量函数拆解为单变量(或部分变量)的多个函数并依次调用。
再直白一点:利用闭包,可以形成一个不销毁的私有作用域,把预先处理的内容都存在这个不销毁的作用域里面,并且返回一个函数,以后需要执行的就是这个函数。
PS: 如果还是不理解也没关系,跟闭包一样不用死扣定义,继续往下面看应用就行了。
柯里化的作用
柯里化有三个常见应用:
- 参数复用- 当在多次调用同一个函数,并且传递的参数大多数是相同的,那么该函数可能是一个很好的柯里化候选
- 提前返回- 多次调用多次内部判断,可以直接把第一次判断的结果返回外部接收
- 延迟计算/运行- 避免重复的去执行程序,等真正需要结果的时候再执行
应用一: 参数复用
如下名为url
的函数,接收三个参数,函数的作用是返回三个参数拼接的字符串。
1 2 3 4 5 6 7
| function url(protocol, hostname, pathname) { return `${protocol}${hostname}${pathname}` }
const url1 = url('http://', 'www.wangyan.ltd', 'blogs') console.log(url1)
|
上面这种写法的弊端是: 当我们有很多网址时,会导致非常多重复的参数(比如 http://
就是重复的参数,我们在浏览器里面输入网址也不需要输入http或者https)。
利用柯里化实现参数复用的思路:
- 原函数(称为函数A)只设置一个参数(接收协议这个参数);
- 在函数内部在创建一个函数(称为函数B),函数B把刚才剩余的两个参数给补上,并返回字符串形式的url;
- 函数A返回函数B.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function url_curring(protocol) { return function(hostname, pathname) { return `${protocol}${hostname}${pathname}` } }
const url_http = url_curring('http://')
const url1 = url_http('www.fedbook.cn', '/frontend-languages/javascript/function-currying/') const url2 = url_http('www.fedbook.cn', '/handwritten/javascript/10-实现bind方法/') const url3 = url_http('www.wangyan.ltd', '/')
console.log(url1) console.log(url2) console.log(url3)
|
应用二: 兼容性检测
以下代码为了编写方便,会使用ES6的语法。实际生产环境中如果要做兼容性检测功能。需要转换成ES5语法。
因为浏览器发展和各种原因,有些函数和方法是不被部分浏览器支持的,此时需要提前进行判断。从而确定用户的浏览器是否支持相应的方法。
以下事件监听为例, IE(IE9)之前支持的是 attachEvent
方法,其它主流浏览器支持的是 addEventListener
方法,我们需要创建一个新的函数来进行两者的判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const addEvent = function(element, type, listener, useCapture) { if(window.addEventListener) { console.log('判断为其他浏览器') element.addEventListener(type, function(e) { listener.call(element, e) }, useCapture) }else if(window.attachEvent) { console.log('判断为 IE9 以下的浏览器') element.attachEvent('on' + type, function(e) { listener.call(element, e) }) } }
let div = document.querySelector('div') let p = document.querySelector('p') let span = document.querySelector('span')
addEvent(div, 'click', (e) => { console.log('点击了div')}, true) addEvent(p, 'click', (e) => { console.log('点击了p')}, true) addEvent(span, 'click', (e) => { console.log('点击了span')}, true)
|
上面这种封装的弊端是: 每次写监听事件的时候调用 addEvent
函数,都会进行 if……else……
的兼容性判断。事实上在代码中只需执行一次兼容性判断就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。
那么怎么用函数柯里化优化这个封装函数?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const addEvent = (function() { if(window.addEventListener) { console.log('判断为其他浏览器') return function(element, type, listener, useCapture) { element.addEventListener(type, function(e) { listener.call(element, e) },useCapture) } }else if(window.attachEvent) { return function(element, type, listener) { console.log('判断为 IE9 以下浏览器') element.attachEvent('on' + type, function(e) { listener.call(element. e) }) } } }) ()
let div = document.querySelector('div') let p = document.querySelector('p') let span = document.querySelector('span')
addEvent(div, 'click', (e) => { console.log('点击了 div')}, true) addEvent(p, 'click', (e) => { console.log('点击了 p')}, true) addEvent(span, 'click', (e) => { console.log('点击了 span')}, true)
|
应用三: 实现一个add函数
这是一道经典的面试题,要求我们实现一个add函数们可以实现以下计算结果:
1 2 3
| add(1)(2)(3) = 6 add(1,2,3)(4) = 10 add(1)(2)(3)(4)(5) = 15
|
通过这道题正好可以解释柯里化的延迟执行,直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function add() { let args = Array.prototype.slice.call(arguments)
let inner = function() { args.push(...arguments) return inner }
inner.toString = function() { return args.reduce((prev, cur, index) => { return prev + cur }) }
return inner }
let result = add(1)(2)(3)(4) console.log(result)
|
解释几个关键点:
因为 arguments
并不是一个真正的数组,而是一个数组类似对象(拥有length属性),Array.prototype.slice.call(arguments)
能将具有 length
属性的对象(伪数组)转成数组。
对于 add(1)(2)(3)(4)
,执行每个括号的时候都返回 inner
函数,不断自己调用自己,每次内部函数返回的都是内部函数。
如果打印函数执行的最终返回结果,可以发现返回了一个字符串(原本函数被转换为字符串返回了), 这即是发生了隐式转换,而发生隐式转换是因为调用了内部的 toString
方法。
知道了这一点,我们就可以利用这个特性自定义返回的内容:重写 inner
函数的 toString
方法,在里面实现参数相加的执行代码。
值得一提的是,这种处理后能返回正确的累加结果,但返回的结果是一个函数类型( function
),这是因为我们在用递归返回函数,内部函数在被隐式转换为字符串之前本来就是一个函数。