详解JS函数柯里化

详解JS函数柯里化

Posted by SkioFox on September 22, 2017

函数柯里化

定义:什么是函数柯里化?

专业术语解释:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

看这个解释有一点抽象,看如下add函数实现。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。

为什么使用函数柯里化?作用是什么?

  1. 参数复用

     // 正常正则验证字符串 reg.test(txt)
    
     // 函数封装后
     function check(reg, txt) {
         return reg.test(txt)
     }
    
     check(/\d+/g, 'test')       //false
     check(/[a-z]+/g, 'test')    //true
    
     // Currying后
     function curryingCheck(reg) {
         return function(txt) {
             return reg.test(txt)
         }
     }
    
     var hasNumber = curryingCheck(/\d+/g)
     var hasLetter = curryingCheck(/[a-z]+/g)
    
     hasNumber('test1')      // true
     hasNumber('testtest')   // false
     hasLetter('21212')      // false
    

    上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用。

  2. 提前确认

     var on = function(element, event, handler) {
         if (document.addEventListener) {
             if (element && event && handler) {
                 element.addEventListener(event, handler, false);
             }
         } else {
             if (element && event && handler) {
                 element.attachEvent('on' + event, handler);
             }
         }
     }
    
     var on = (function() {
         if (document.addEventListener) {
             return function(element, event, handler) {
                 if (element && event && handler) {
                     element.addEventListener(event, handler, false);
                 }
             };
         } else {
             return function(element, event, handler) {
                 if (element && event && handler) {
                     element.attachEvent('on' + event, handler);
                 }
             };
         }
     })();
    
     //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
     var on = function(isSupport, element, event, handler) {
         isSupport = isSupport || document.addEventListener;
         if (isSupport) {
             return element.addEventListener(event, handler, false);
         } else {
             return element.attachEvent('on' + event, handler);
         }
     }
    

    上面是普通的DOM封装。上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

  3. 延迟运行

     Function.prototype.bind = function (context) {
         var _this = this
         var args = Array.prototype.slice.call(arguments, 1)
        
         return function() {
             return _this.apply(context, args)
         }
     }
    

    像我们js中经常使用的bind,实现的机制就是Currying.

  4. 如何封装Currying函数

     // 初步封装
     var currying = function(fn) {
         // args 获取第一个方法内的全部参数
         var args = Array.prototype.slice.call(arguments, 1)
         return function() {
             // 将后面方法里的全部参数和args进行合并
             var newArgs = args.concat(Array.prototype.slice.call(arguments))
             // 把合并后的参数通过apply作为fn的参数并执行
             return fn.apply(this, newArgs)
         }
     }
    

    首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。 这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

     // 支持多参数传递
     function progressCurrying(fn, args) {
    
         var _this = this
         var len = fn.length;
         var args = args || [];
    
         return function() {
             var _args = Array.prototype.slice.call(arguments);
             Array.prototype.push.apply(args, _args);
    
             // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
             if (_args.length < len) {
                 return progressCurrying.call(_this, fn, _args);
             }
    
             // 参数收集完毕,则执行fn
             return fn.apply(this, _args);
         }
     }
    

    其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

  5. 经典面试题

     // 实现一个add方法,使计算结果能够满足如下预期:
     add(1)(2)(3) = 6;
     add(1, 2, 3)(4) = 10;
     add(1)(2)(3)(4)(5) = 15;
    
     function add() {
         // 第一次执行时,定义一个数组专门用来存储所有的参数
         // 因为arguments并不是真正的数组对象,只是与数组类似而已,所以它并没有slice这个方法,Array.prototype.slice.call(arguments, 1)可以理解成是让arguments转换成一个数组对象,让arguments具有slice()方法。
         var _args = Array.prototype.slice.call(arguments);
    
         // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
         var _adder = function() {
             _args.push(...arguments);
             return _adder;
         };
    
         // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
         _adder.toString = function () {
             return _args.reduce(function (a, b) {
                 return a + b;
             });
         }
         return _adder;
     }
    
     add(1)(2)(3)                // 6
     add(1, 2, 3)(4)             // 10
     add(1)(2)(3)(4)(5)          // 15
     add(2, 6)(1)                // 9
    
  6. 封装 axios

     // 封装 axios 用函数柯里化的方法将参数提前固化在内部
     export function get(url) {
         return function(params = {}) {
             // 返回的是promise
             return axios.get(baseUrl + url, {
             params
             }).then((res) => {
             const {errno, data} = res.data
             if (errno === ERR_OK) {
                 return data
             }
             }).catch((e) => {
             })
         }
     }