higher-order-function

柯里化

柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

核心思想是把多参数传入的函数拆成单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。

通用实现:

var currying = function (fn) {
  var args = [].slice.call(arguments, 1)
  return function () {
    var newArgs = args.concat([].slice.call(arguments))
    return fn.apply(null, newArgs)
  }
}

柯里化有 3 个常见作用:

  • 提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性。
  • 提前返回
  • 延迟计算/运行

参数复用

// 原始的加法函数
function origPlus(a, b) {
  return a + b
}

// 柯里化后的plus函数
function plus(a) {
  return function (b) {
    return a + b
  }
}

// ES6写法
const plus = (a) => (b) => a + b
plus(1)(2) // 3

//
let plus1 = plus(1)
plus1(2) // 3
plus1(3) // 4

提前返回

常见的兼容现代浏览器以及 IE 浏览器的事件添加方法

var addEvent = function (el, type, fn, capture) {
  if (window.addEventListener) {
    el.addEventListener(
      type,
      function (e) {
        fn.call(el, e)
      },
      capture
    )
  } else if (window.attachEvent) {
    el.attachEvent('on' + type, function (e) {
      fn.call(el, e)
    })
  }
}

很显然,我们每次使用 addEvent 为元素添加事件的时候,都会走一遍 if...else if ...,其实只要一次判定就可以了。

// 柯里化
var addEvent = (function () {
  if (window.addEventListener) {
    return function (el, event, fn, capture) {
      el.addEventListener(
        event,
        function (e) {
          fn.call(el, e)
        },
        capture
      )
    }
  } else if (window.attachEvent) {
    return function (el, event, fn, capture) {
      el.attachEvent('on' + event, function (e) {
        fn.call(el, e)
      })
    }
  }
})()

延迟计算/运行

const curry = function (fn) {
  const _args = []
  return function cb(...rest) {
    if (rest.length === 0) {
      return fn.apply(this, _args)
    }
    _args.push(...rest)
    return cb
  }
}

const curryAdd = curry((...T) => T.reduce((sum, single) => (sum += single)))
curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd() // 最后计算输出:6

Function.prototype.bind 方法也是柯里化应用

if (!function () {}.bind) {
  Function.prototype.bind = function (context) {
    var self = this,
      args = Array.prototype.slice.call(arguments)

    return function () {
      return self.apply(context, args.slice(1))
    }
  }
}

关于柯里化的性能

柯里化使用了闭包,就会存在闭包的缺点,可能会造成内存泄漏。

高阶柯里化函数

// es5
function curryingHelper(fn) {
  var _args = Array.prototype.slice.call(arguments, 1)
  return function () {
    var _newArgs = Array.prototype.slice.call(arguments)
    var _totalArgs = _args.concat(_newArgs)
    return fn.apply(this, _totalArgs)
  }
}

// es6
function curryingHelper(fn, ...args) {
  return (...newArgs) => {
    return fn.apply(this, args.concat(newArgs))
  }
}

写一个简单的函数验证上面的辅助柯里化函数的正确性, 代码部分如下:

function showMsg(name, age, fruit) {
  console.log(
    'My name is ' +
      name +
      ", I'm " +
      age +
      ' years old, ' +
      ' and I like eat ' +
      fruit
  )
}

var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple')
curryingShowMsg1(22, 'apple') // My name is dreamapple, I'm 22 years old,  and I like eat apple

var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20)
curryingShowMsg2('watermelon') // My name is dreamapple, I'm 20 years old,  and I like eat watermelon

我们希望那些经过柯里化后的函数可以每次只传递进去一个参数,然后可以进行多次参数的传递,那么应该怎么办呢?

// es5
function betterCurryingHelper(fn, len) {
  var length = len || fn.length // fn.length表示的是这个函数的参数长度
  return function () {
    var allArgsFulfilled = arguments.length >= length

    // 如果参数全部满足,就可以终止递归调用
    if (allArgsFulfilled) {
      return fn.apply(this, arguments)
    } else {
      var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments))
      return betterCurryingHelper(
        curryingHelper.apply(this, argsNeedFulfilled),
        length - arguments.length
      )
    }
  }
}

// es6
function betterCurryingHelper(fn, length = fn.length) {
  return (...args) => {
    let allArgsFulfilled = args.length >= length

    // 如果参数全部满足,就可以终止递归调用
    if (allArgsFulfilled) {
      return fn.apply(this, args)
    } else {
      let argsNeedFulfilled = [fn].concat(args)
      return betterCurryingHelper(
        curryingHelper.apply(this, argsNeedFulfilled),
        length - args.length
      )
    }
  }
}

检验一下这个函数的正确性:

function showMsg(name, age, fruit) {
  console.log(
    'My name is ' +
      name +
      ", I'm " +
      age +
      ' years old, ' +
      ' and I like eat ' +
      fruit
  )
}

var betterShowMsg = betterCurryingHelper(showMsg)
betterShowMsg('dreamapple', 22, 'apple') // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple', 22)('apple') // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple')(22, 'apple') // My name is dreamapple, I'm 22 years old,  and I like eat apple
betterShowMsg('dreamapple')(22)('apple') // My name is dreamapple, I'm 22 years old,  and I like eat apple

更多内容请参阅高阶柯里化函数

新增面试题目:

Q1: 完成一个 sum 函数,使调用后输出 6。

sum(1)(2)(3).valueOf()

// 答案
function add(a, b, c) {
  return {
    valueOf() {
      return a + b + c
    },
  }
}

// 使用上面的 betterCurryingHelper 函数
var sum = betterCurryingHelper(add)

sum(1)(2)(3).valueOf()

javascript 中有趣的反柯里化

Function.prototype.uncurrying = function () {
  var _this = this
  return function () {
    return Function.prototype.call.apply(_this, arguments)
  }
}

功能测试:

let obj = {}
let push = Array.prototype.push.uncurrying()
push(obj, 'first')
console.log(obj) // {0: "first", length: 1}

更多内容请参阅javascript 中有趣的反柯里化

参考:

尾调用

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数

// 尾调用:true
function f(x) {
  return g(x)
}

// 尾调用:false
function f(x) {
  let y = g(x)
  return y
}

// 尾调用:false
function f(x) {
  return g(x) + 1
}

// 尾调用:false
function f(x) {
  g(x)
}

尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

/**
 * [ECMAScript 6 入门](http://es6.ruanyifeng.com/#docs/function#尾递归优化的实现)
 * 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
 * 递归非常耗费内存,但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
 * 不同方式实现菲波那切数列
 *
 * 变形题目:一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
 * 解析 f(n) = f(n-1) + f(n-2); f(2) = 1; f(1) = 1.
 */

// 非尾递归。需要保存n个调用记录,复杂度 O(n)
function Fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return Fibonacci(n - 2) + Fibonacci(n - 1)
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出

// 尾递归。只保留一个调用记录,复杂度 O(1)
function Fibonacci(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  }
  return Fibonacci(n - 1, ac2, ac1 + ac2)
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

// 尾递归优化
function tco(f) {
  var value
  var active = false
  var accumulated = []

  return function accumulator() {
    accumulated.push(arguments)
    if (!active) {
      active = true
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift())
      }
      active = false
      return value
    }
  }
}

let fib = tco(function (n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  }
  return fib(n - 1, ac2, ac1 + ac2)
})

// 不会发生调用栈溢出
fib(100) // 573147844013817200000
fib(1000) // 7.0330367711422765e+208
fib(100000) // Infinity

参考: