柯里化(Curring, 以逻辑学家 Haskell Curry 命名)
写在开头
柯里化理解的基础来源于我们前几篇文章构建的知识, 如果还未能掌握闭包, 建议回阅前文.
代码例子会用到 apply/call , 一般用来实现对象冒充, 例如字符串冒充数组对象, 让字符串拥有数组的方法. 待对象讲解篇会细分解析. 在此先了解, 两者功能相同, 区别在于参数传递方式的不同, apply 参数以数组方式传递, call 多个参数则是逗号隔开.
- apply(context, [arguments]);
- call(context, arg1, arg2, arg3, ....);
代码例子中使用到了 ES6 语法, 对 ES6 还不熟悉的话, 可学习社区这篇文章:《30 分钟掌握 ES6/ES2015 核心内容(上)》
函数柯里化
函数柯里化在 JavaScript 中其实是高阶函数的一种应用, 上篇文章我们简略介绍了高阶函数(可以作为参数传递, 或作为返回值).
理论知识太枯燥, 来个生活小例子,"存款买房"(富二代绕道). 假设买房是我们存钱的终极目标. 那么在买房前, 存在卡里的钱 (老婆本) 就不能动. 等到够钱买房了, 钱从银行卡取出来, 开始买买买...
函数柯里化就像我们往卡里存钱, 存够了, 才能执行买房操作, 存不够, 接着存.
函数柯里化公式
先上几个公式(左边是普通函数, 右边就是转化后柯里化函数支持的调用方式):
- // 公式类型一
- fn(a,b,c,d) => fn(a)(b)(c)(d);
- fn(a,b,c,d) => fn(a, b)(c)(d);
- fn(a,b,c,d) => fn(a)(b,c,d);
- // 公式类型二
- fn(a,b,c,d) => fn(a)(b)(c)(d)();
- fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();
两种公式类型的区别 -- 函数触发执行的机制不同:
公式一当传入参数等于函数参数数量时开始执行
公式二当没有参数传入时 (且参数数量满足) 开始执行
通过公式, 我们先来理解这行代码 fn(a)(b)(c)(d), 执行 fn(a) 时返回的是一个函数, 并且支持传参. 何时返回的是值而不是函数的触发机制控制权在我们手里, 我们可以为函数制定不同的触发机制.
普通的函数调用, 一次性传入参数就执行. 而通过柯里化, 它可以帮我们实现函数部分参数传入执行(并未立即执行原始函数, 钱没存够接着存), 这就是函数柯里化的特点:"延迟执行和部分求值"
"函数柯里化: 指封装一个函数, 接收原始函数作为参数传入, 并返回一个能够接收并处理剩余参数的函数"
函数柯里化的例子
- // 等待我们柯里化实现的方法 add
- function add(a, b, c, d) {
- return a + b + c + d;
- };
- // 最简单地实现函数 add 的柯里化
- // 有点 low, 有助于理解
- function add(a, b, c, d) {
- return function(a) {
- return function(b) {
- return function(c) {
- return a + b + c + d;
- }
- }
- }
- }
分析代码知识点:
函数作为返回值返回, 闭包形成, 外部环境可访问函数内部作用域
子函数可访问父函数的作用域, 作用域由内而外的作用域链查找规则, 作用域嵌套形成
在函数参数数量不满足时, 返回一个函数(该函数可接收并处理剩余参数)
当函数数量满足我们的触发机制(可自由制定), 触发原始函数执行
前几篇文章的知识点此时刚好. 可见基础知识的重要性, 高阶的东西始终要靠小砖头堆砌出来.
弄清原理后, 接下来就是将代码写得更通用些(高大上些).
- // 公式类型一: 参数数量满足函数参数要求, 触发执行
- // fn(a,b,c,d) => fn(a)(b)(c)(d);
- const createCurry = (fn, ...args) {
- let _args = args || [];
- let length = fn.length; // fn.length 代码函数参数数量
- return (...REST) => {
- let _allArgs = _args.slice(0);
- // 深拷贝闭包共用对象_args, 避免后续操作影响(引用类型)
- _allArgs.push(...REST);
- if (_allArgs.length <length) {
- // 参数数量不满足原始函数数量, 返回 curry 函数
- return createCurry.call(this, fn, ..._allArgs);
- } else {
- // 参数数量满足原始函数数量, 触发执行
- return fn.apply(this, _allArgs);
- }
- }
- }
- const curryAdd = createCurry(add, 2);
- let sum = curryAdd(3)(4)(5); // 14
- // ES5 写法
- function createCurry() {
- var fn = arguments[0];
- var _args = [].slice.call(arguments, 1);
- var length = fn.length;
- return function() {
- var _allArgs = _args.slice(0);
- _allArgs = _allArgs.concat([].slice.call(arguments));
- if (_allArgs.length < length) {
- _allArgs.unshift(fn);
- return createCurry.apply(this, _allArgs);
- } else {
- return fn.apply(this, _allArgs);
- }
- }
- }
- // 公式类型二: 无参数传入时并且参数数量已经满足函数要求
- // fn(a, b, c, d) => fn(a)(b)(c)(d)();
- // fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn();
- const createCurry = (fn, ...args) => {
- let all = args || [];
- let length = fn.length;
- return (...REST) => {
- let _allArgs = all.slice(0);
- _allArgs.push(...REST);
- if (REST.length> 0 || _allArgs.length <length) {
- // 调用时参数不为空或存储的参数不满足原始函数参数数量时, 返回 curry 函数
- return createCurry.call(this, fn, ..._allArgs);
- } else {
- // 调用参数为空(), 且参数数量满足时, 触发执行
- return fn.apply(this, _allArgs);
- }
- }
- }
- const curryAdd = createCurry(add, 2);
- let sum = curryAdd(3)(4)(5)(); // 14
- // ES5 写法
- function createCurry() {
- var fn = arguments[0];
- var _args = [].slice.call(arguments, 1);
- var length = fn.length;
- return function() {
- var _allArgs = _args.slice(0);
- _allArgs = _allArgs.concat([].slice.call(arguments));
- if (arguments.length> 0 || _allArgs.length <length) {
- _allArgs.unshift(fn);
- return createCurry.apply(this, _allArgs);
- } else {
- return fn.apply(this, _allArgs);
- }
- }
- }
为实现公式中不同的两种调用公式, 两个 createCurry 方法制定了两种不同的触发机制. 记住一个点, 函数触发机制可根据需求自行制定.
偏函数与柯里化的区别
先上个公式看对比:
- // 函数柯里化: 参数数量完整
- fn(a,b,c,d) => fn(a)(b)(c)(d);
- fn(a,b,c,d) => fn(a,b)(c)(d);
- // 偏函数: 只执行了部分参数
- fn(a,b,c,d) => fn(a);
- fn(a,b,c,d) => fn(a, b);
- "函数柯里化中, 当你传入部分参数时, 返回的并不是原始函数的执行结果, 而是一个可以继续支持后续参数的函数. 而偏函数的调用方式更像是普通函数的调用方式, 只调用一次, 它通过原始函数内部来实现不定参数的支持."
如果已经看懂上述柯里化的代码例子, 那么改写支持偏函数的代码, 并不难.
- // 公式:
- // fn(a, b, c, d) => fn(a);
- // fn(a, b, c, d) => fn(a,b,c);
- const partialAdd = (a = 0, b = 0, c = 0, d = 0) => {
- return a + b + c +d;
- }
- partialAdd(6); // 6
- partialAdd(2, 3); // 5
使用 ES6 函数参数默认值, 为没有传入参数, 指定默认值为 0, 支持无参数或不定参数传入.
柯里化的特点:
参数复用(固定易变因素)
延迟执行
提前返回
柯里化的缺点
柯里化是牺牲了部分性能来实现的, 可能带来的性能损耗:
存取 arguments 对象要比存取命名参数要慢一些
老版本浏览器在 arguments.lengths 的实现相当慢(新版本浏览器忽略)
fn.apply() 和 fn.call() 要比直接调用 fn() 慢
大量嵌套的作用域和闭包会带来开销, 影响内存占用和作用域链查找速度
柯里化的应用
利用柯里化制定约束条件, 管控触发机制
处理浏览器兼容(参数复用实现一次性判断)
函数节流防抖(延迟执行)
ES5 前 bind 方法的实现
一个应用例子: 浏览器事件绑定的兼容处理
- // 普通事件绑定函数
- var addEvent = function(ele, type, fn, isCapture) {
- if(Windows.addEventListener) {
- ele.addEventListener(type, fn, isCapture)
- } else if(Windows.attachEvent) {
- ele.attachEvent("on" + type, fn)
- }
- }
- // 弊端: 每次调用 addEvent 都会进行判断
- // 柯里化事件绑定函数
- var addEvent = (function() {
- if(Windows.addEventListener) {
- return function(ele, type, fn, isCapture) {
- ele.addEventListener(type, fn, isCapture)
- }
- } else if(Windows.attachEvent) {
- return function(ele, type, fn) {
- ele.attachEvent("on" + type, fn)
- }
- }
- })()
- // 优势: 判断只执行一次, 通过闭包保留了父级作用域的判断结果
秒懂反柯里化
先上公式, 从来没有这么喜欢写公式, 简明易懂.
- // 反柯里化公式:
- curryFn(a)(b)(c)(d) = fn(a, b, c, d);
- curryFn(a) = fn(a);
看完公式, 是不是似曾相识, 这不就是我们日常敲码的普通函数么? 没错的, 函数柯里化就是把普通函数变成成一个复杂的函数, 而反柯里化其就是柯里化的逆反, 把复杂变得简单.
函数柯里化是把支持多个参数的函数变成接收单一参数的函数, 并返回一个函数能接收处理剩余参数: fn(a,b,c,d) => fn(a)(b)(c)(d), 而反柯里化就是把参数全部释放出来: fn(a)(b)(c)(d) => fn(a,b,c,d).
- // 反柯里化: 最简单的反柯里化(普通函数)
- function add(a, b, c, d) {
- return a + b + c + d;
- }
反思: 为何要使用柯里化
函数柯里化是函数编程中的一个重要的基础, 它为我们提供了一种编程的思维方式. 显然, 它让我们的函数处理变得复杂, 代码调用方式并不直观, 还加入了闭包, 多层作用域嵌套, 会有一些性能上的影响.
但在一些复杂的业务逻辑封装中, 函数柯里化能够为我们提供更好的应对方案, 让我们的函数更具自由度和灵活性.
实际开发中, 如果你的逻辑处理相对复杂, 不妨换个思维, 用函数柯里化来实现, 技能包不嫌多.
说到底, 程序员就是解决问题的那群人.
写在结尾
本篇函数柯里化知识点的理解确实存在难度, 暂时跳过这章也无妨, 可以先了解再深入. 耐得主寂寞的小伙伴回头多啃几遍, 没准春季面试就遇到了.
参考文档:
JS 高阶函数应用 - 函数柯里化和反柯里化 https://www.cnblogs.com/yifeng555/p/8878474.html
前端基础进阶(八): 深入详解函数的柯里化 https://www.jianshu.com/p/5e1899fe7d6b
系列更文请关注专栏:《前端进击的巨人》, 不断更新中...
本文首发 GitHub, 期待 Star!
https://github.com/ZengLingYong/blog
来源: https://segmentfault.com/a/1190000017981474