js 高阶函数之柯里化
博客地址:https://ainyi.com/74
定义
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
就是只传递给函数某一部分参数来调用,返回一个新函数去处理剩下的参数(闭包)
常用的封装成 add 函数
// reduce 方法
const add = (...args) => args.reduce((a, b) => a + b)
// 传入多个参数,执行 add 函数
add(1, 2) // 3
// 假设有一个 currying 函数
let sum = currying(params)
sum(1)(3) // 4
实际应用
延迟计算
部分求和例子,说明了延迟计算的特点
const add = (...args) => args.reduce((a, b) => a + b)
// 简化写法
function currying(func) {
const args = []
return function result(...rest) {
if (rest.length === 0) {
return func(...args)
} else {
args.push(...rest)
return result
}
}
}
const sum = currying(add)
sum(1, 2)(3) // 未真正求值,收集参数的和
sum(4) // 未真正求值,收集参数的和
sum() // 输出 10
上面的代码理解:先定义 add 函数,然后 currying 函数就是用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
上面的 currying 函数是一种简化写法,判断传入的参数长度是否为 0,若为 0 执行函数,否则收集参数到 args 数组
另一种常见的应用是 bind 函数,我们看下 bind 的使用
let obj = {
name: 'Krry'
}
const fun = function () {
console.log(this.name)
}.bind(obj)
fun() // Krry
这里 bind 用来改变函数执行时候的上下文this,但是函数本身并不执行,所以本质上是延迟计算,这一点和 call / apply 直接执行有所不同
动态创建函数
有一种典型的应用情景是这样的,每次调用函数都需要进行一次判断,但其实第一次判断计算之后,后续调用并不需要再次判断,这种情况下就非常适合使用柯里化方案来处理
即第一次判断之后,动态创建一个新函数用于处理后续传入的参数,并返回这个新函数。当然也可以使用惰性函数来处理,本例最后一个方案会介绍
我们看下面的这个例子,在 DOM 中添加事件时需要兼容现代浏览器和 IE 浏览器(IE < 9),方法就是对浏览器环境进行判断,看浏览器是否支持,简化写法如下
// 简化写法
function addEvent (type, el, fn, capture = false) {
if (window.addEventListener) {
el.addEventListener(type, fn, capture);
}
else if(window.attachEvent) {
el.attachEvent('on' + type, fn);
}
}
但是这种写法有一个问题,就是每次添加事件都会调用做一次判断,比较麻烦
可以利用闭包和立即调用函数表达式(IIFE)来实现只判断一次,后续都无需判断
const addEvent = (function(){
if (window.addEventListener) {
return function (type, el, fn, capture) { // 关键
el.addEventListener(type, fn, capture)
}
}
else if(window.attachEvent) {
return function (type, el, fn) { // 关键
el.attachEvent('on' + type, fn)
}
}
})()
上面这种实现方案就是一种典型的柯里化应用,在第一次的 if...else if... 判断之后完成第一次计算,然后动态创建返回新的函数用于处理后续传入的参数
这样做的好处就是之后调用之后就不需要再次调用计算了
当然可以使用惰性函数来实现这一功能,原理很简单,就是重写函数
function addEvent (type, el, fn, capture = false) {
// 重写函数
if (window.addEventListener) {
addEvent = function (type, el, fn, capture) {
el.addEventListener(type, fn, capture);
}
}
else if(window.attachEvent) {
addEvent = function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
// 执行函数,有循环爆栈风险
addEvent(type, el, fn, capture);
}
第一次调用 addEvent 函数后,会进行一次环境判断,在这之后 addEvent 函数被重写,所以下次调用时就不会再次判断环境
参数复用
我们知道调用 toString() 可以获取每个对象的类型,但是不同对象的 toString() 有不同的实现
所以需要通过 Object.prototype.toString() 来获取 Object 上的实现
同时以 call() / apply() 的形式来调用,并传递要检查的对象作为第一个参数
例如下面这个例子
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
function isNumber(obj) {
return Object.prototype.toString.call(obj) === '[object Number]';
}
function isString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
// Test
isArray([1, 2, 3]) // true
isNumber(123) // true
isString('123') // true
但是上面方案有一个问题,那就是每种类型都需要定义一个方法,这里我们可以使用 bind 来扩展,优点是可以直接使用改造后的 toStr
const toStr = Function.prototype.call.bind(Object.prototype.toString);
// 改造前直接调用
[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
123.toString() // SyntaxError: Invalid or unexpected token
Object(123).toString() // "123"
// 改造后调用 toStr
toStr([1, 2, 3]) // "[object Array]"
toStr('123') // "[object String]"
toStr(123) // "[object Number]"
toStr(Object(123)) // "[object Number]"
上面例子首先使用 Function.prototype.call 函数指定一个 this 值,然后 .bind 返回一个新的函数,始终将 Object.prototype.toString 设置为传入参数,其实等价于 Object.prototype.toString.call()
实现 Currying 函数
可以理解所谓的柯里化函数,就是封装一系列的处理步骤,通过闭包将参数集中起来计算,最后再把需要处理的参数传进去
实现原理就是用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
上面延迟计算部分已经实现了一个简化版的 Currying 函数
下面实现一个更加健壮的 Currying 函数
function currying(fn, length) {
// 第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
length = length || fn.length
return function (...args) { // 返回一个新函数,接收参数为 ...args
// 新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
return args.length >= length
? fn.apply(this, args) // 满足要求,执行 fn 函数,传入新函数的参数
: currying(fn.bind(this, ...args), length - args.length)
// 不满足要求,递归 currying 函数
// 新的 fn 为 bind 返回的新函数,新的 length 为 fn 剩余参数的长度
}
}
// Test
const fn = currying(function(a, b, c) {
console.log([a, b, c]);
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
上面使用的是 ES5 和 ES6 的混合语法
那如果不想使用 call/apply/bind 这些方法呢,自然是可以的,看下面的 ES6 极简写法,更加简洁也更加易懂
const currying = fn =>
judge = (...args) =>
args.length >= fn.length
? fn(...args)
: (...arg) => judge(...args, ...arg)
// Test
const fn = currying(function(a, b, c) {
console.log([a, b, c]);
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
如果还很难理解,看下面例子
function currying(fn, length) {
length = length || fn.length;
return function (...args) {
return args.length >= length
? fn.apply(this, args)
: currying(fn.bind(this, ...args), length - args.length)
}
}
const add = currying(function(a, b, c) {
console.log([a, b, c].reduce((a, b) => a + b))
})
add(1, 2, 3) // 6
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2, 3) // 6
扩展:函数参数 length
函数 currying 的实现中,使用了 fn.length 来表示函数参数的个数,那 fn.length 表示函数的所有参数个数吗?并不是
函数的 length 属性获取的是形参的个数,但是形参的数量不包括剩余参数个数,而且仅包括第一个具有默认值之前的参数个数,看下面的例子
((a, b, c) => {}).length; // 3
((a, b, c = 3) => {}).length; // 2
((a, b = 2, c) => {}).length; // 1
((a = 1, b, c) => {}).length; // 0
((...args) => {}).length; // 0
const fn = (...args) => {
console.log(args.length);
}
fn(1, 2, 3) // 3
所以在柯里化的场景中,不建议使用 ES6 的函数参数默认值
const fn = currying((a = 1, b, c) => {
console.log([a, b, c])
})
fn() // [1, undefined, undefined]
fn()(2)(3) // Uncaught TypeError: fn(...) is not a function
我们期望函数 fn 输出 1, 2, 3,但是实际上调用柯里化函数时 ((a = 1, b, c) => {}).length === 0
所以调用 fn() 时就已经执行并输出了 1, undefined, undefined,而不是理想中的返回闭包函数
所以后续调用 fn()(2)(3) 将会报错
小结&链接
定义:柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术
实际应用
- 延迟计算:部分求和、bind 函数
- 动态创建函数:添加监听 addEvent、惰性函数
- 参数复用:Function.prototype.call.bind(Object.prototype.toString)
实现 Currying 函数:用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
函数参数 length:获取的是形参的个数,但是形参的数量不包括剩余参数个数,而且仅包括第一个参数有默认值之前的参数个数
参考文章:JavaScript专题之函数柯里化
博客地址:https://ainyi.com/74
js 高阶函数之柯里化的更多相关文章
- 理解运用JS的闭包、高阶函数、柯里化
JS的闭包,是一个谈论得比较多的话题了,不过细细想来,有些人还是理不清闭包的概念定义以及相关的特性. 这里就整理一些,做个总结. 一.闭包 1. 闭包的概念 闭包与执行上下文.环境.作用域息息相关 执 ...
- JS的闭包、高阶函数、柯里化
本文原链接:https://cloud.tencent.com/developer/article/1326958 https://cloud.tencent.com/developer/articl ...
- python 高阶函数、柯里化
高阶函数 First Class Object 函数在python中是一等公民 函数也是对象,可调用的对象 函数可作为普通变量.参数.返回值等等 高阶函数 数学概念 y=g(f(x)) 在数学和计算机 ...
- Python进阶1---高阶函数、柯里化
高阶函数 不相等 自定义sort函数 内建函数--高阶函数 #sort函数 def sort2(lst,key = None,reverse = False): res = [] if key is ...
- 高阶JS---函数柯里化
什么是函数柯里化? 百度百科: 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术.通 ...
- js高阶函数--判断数据类型、函数胡柯里化;
一.判断数据类型: 常见的判断有typeof.instanceof. constructor. prototype,先来看typeof: var a = "hello world" ...
- JS 函数的柯里化与反柯里化
===================================== 函数的柯里化与反柯里化 ===================================== [这是一篇比较久之前的总 ...
- 浅析 JavaScript 中的 函数 currying 柯里化
原文:浅析 JavaScript 中的 函数 currying 柯里化 何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字 ...
- 高频重要前端API手写整理(call,apply,bind,instanceof,flat,filter,new,防抖,节流,深浅拷贝,数组乱序,数组去重,继承, lazyman,jsonp的实现,函数的柯里化 )
Function.prototype.call = function(context,...args){ var context = context || window; context.fn = t ...
随机推荐
- ThinkPHP中文字段问题
转自: https://www.baidu.com/link?url=Ohc9epgQgkNYLwnHqP-jZ9RfIQWW50-iz8-ZMIPLdtCIJHnUpYwQnDLmXzi7Fa110 ...
- ImportError: this is MySQLdb version (1, 2, 5, 'final', 1), but _mysql is version (1, 4, 4, 'final', 0)
(flask-demo) ➜ flask-demo git:(master) ✗ pip install mysqlclient==1.2.5 DEPRECATION: Python 2.7 will ...
- xgboost 算法总结
xgboost有一篇博客写的很清楚,但是现在网址已经失效了,之前转载过,可以搜索XGBoost 与 Boosted Tree. 现在参照这篇,自己对它进行一个总结. xgboost是GBDT的后继算法 ...
- Linux 磁盘管理_016
以5个方面讲解 1. 硬盘 2. 磁盘RAID.LVM等 3. 磁盘分区 4. 磁盘格式化 5. 磁盘挂载后磁盘管理 一.硬盘 硬盘分类 备注 机械硬盘 IDE SCSI SATA SAS 固态 ...
- Linux 修改用户的JDK版本
1. vi .bash_profile 2.复制以下到bash_profile 文件,并将此文件里原来的JAVA_HOME和PATH删掉 JAVA_HOME=/java/jdk1..0_22 JRE ...
- Python - importlib 模块
importlib 模块可以根据字符串来导入相应的模块 目录结构: 在根目录下创建 importlib_test.py 和 aaa/bbb.py bbb.py: class Person(object ...
- laydate.render报错:日期格式不合法
在使用laydate渲染日期时: laydate.render({ elem: '#day' }); 提示日期格式不合法 需要使用 too.dateType()来包装 <input type=& ...
- vue播放mu38视频兼容谷歌ie等浏览器
<template> <div id="id_test_video" style="width:100%; height:auto;"> ...
- 构建一个java环境的centos系统镜像并上传到阿里云镜像仓库
编辑dockerfile 文件 FROM centos MAINTAINER zhaoweifeng ENV LANG en_US.UTF-8 RUN /bin/cp /usr/share/zonei ...
- Docker Compose 部署Nginx服务实现负载均衡
Compose简介: Compose是Docker容器进行编排的工具,定义和运行多容器的应用,可以一条命令启动多个容器,使用Docker Compose,不再需要使用shell脚本来启动容器.Comp ...