你不得不知道的JS柯里化与反柯里化

2020-02-24  本文已影响0人  hellomyshadow

柯里化

什么是柯里化(Currying)

维基百科的解释:把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。其由数学家Haskell Brooks Curry提出,并以 curry 命名。

柯里化是指将使用多个参数的函数转换成一系列使用一个参数的函数的技术(又称为部分求值)。
目的:为了缩小适用范围,创建一个针对性更强的函数

特点:

好处:

简单的说,柯里化函数持续地返回一个新函数直到所有的参数用尽为止。这些参数全部保持“活着”的状态(通过闭包),然后当柯里化链中的最后一个函数被返回和执行时会全部被用来执行。
这和 高阶组件Higher-order functions如出一辙。前者返回一个新函数,后者返回一个新组件。

举个简单的栗子

一个计算体积的函数:

function volume (l, w, h) {
  return l * w * h
}

再来创建一个柯里化版本的函数:

function volume (l) {
  return (w) => {
    return (h) => {
      return l * w * h
    }
  }
}
volume(1)(2)(3) // 6

我们把 volume(1)(2)(3) 分割一下来帮助理解:

const vo1 = volume(1)
const vo2 = vo1(2)
const result = vo2(3)
console.log(result)  // 6

作为一个嵌套函数,vo2能够访问到外部的两个函数volumevo1的作用域。这就是为什么vo2能利用定义在已经‘离场’的函数中的参数来进行乘法操作的原因。即使这些函数早已返回并且从内存中垃圾回收了,但其变量仍然保持‘活着’(闭包)。你可以看到 3 个数字每次只有 1 个提供给函数,并同时返回一个新函数,直到所有的数字用尽为止。

这个例子说明,使用柯里化思想能在遇到只能确定一个参数而无法确定其他参数时,代码设计编的变得更方便与高效,达到提升性能的目的。

柯里化背后的逻辑就是获取一个函数并派生出一个返回特殊函数的函数,它实际上是一种思想,或者说是一种程序设计模式

通用的柯里函数

建立一个函数来接受任何函数并且返回柯里化版本的函数:

function curry (fn, ...args) {
  return (..._args) => {
    return fn(...args, ..._args)
  }
}

计算一个长度为100米的物体体积:

function volume (l, h, w) {
  return l * h * w
}
const hCy = curry(volume, 100)
hCy(200, 900) // 18000000
hCy(70, 60) // 420000

复杂的问题变得简单明了!

使用递归实现curry函数

JS柯里化作为函数式编程的重要一环,频繁在算法题中出现。以上的通用柯里化函数还不够完善,我们希望只给curry函数传递一个fn就能达到目的,现在我们使用递归来实现:

function curry (fn) {
  const cy = (...args) => (args.length === fn.length) ?
            fn(...args) : (..._args) => cy(...args, ..._args);
  return cy;
}

fn.length 为函数fn 的形参个数。

bind 与 柯里化

原生JS中的 bind()ES5中被引入,bind()的实现略复杂,它不同于 call()apply() 只是单纯地设置 this 的值后 再传参,它还会将传入的实参(第一个参数之后的参数)与 this 一起绑定,作为返回函数的起始实参。

function sum(x, y) {
    return x + y
}
// 让 this 指向 null,其后的实参(1)也会作为实参传入被绑定的函数sum
var succ = sum.bind(null, 1)
succ(2)  // => 3;
// 可以看到,bind() 传入的实参 1 被绑定到了sum(x, y)中的形参x,
// 返回函数succ传入的实参 2 被绑定给形参y

bind()所返回函数的 length(形参数量) 等于 原函数的形参数量 减去 传入bind()中的实参数量,也就是剩下没有传入的参数,这是因为传入bind()的实参都会依次绑定到原函数的形参上。

function fn(a, b, c, d) {  // fn.length = 4
    //...
}
// 这里通过 bind() 输入了两个实参(1, 2),被绑定到 fn() 上的(a, b)
var after = fn.bind(null, 1, 2)
console.log(after.length)  // ==> 2  返回函数after的形参只剩下(c, d)两个

bind() 的返回函数用作构造函数时,传入 bind()this 将被忽略,其他实参则会全部传入原函数

function original(x) {
    this.a = 1;
    this.b = function() {
        return this.a + x
    }
}
var obj = {
    a: 10
}
// bind()绑定了obj 【this】,并传入(绑定)实参2
var newObj = new (original.bind(obj, 2))

console.log(newObj.a)  // ==> 1,而不是输出绑定obj(this)中的 a (= 10)
// 说明返回函数用作构造函数时,this的值(obj)会被忽略

console.log(newObj.b())  // ==> 3
// 说明传入的实参2 被传给了原函数original 中的形参x

bind() 结合 ...restfn.length 实现柯里化

function curry(targetfn) {
    // 获取原函数的形参个数
    var numOfArgs = targetfn.length;
    return function fn(...rest) {
        if (rest.length < numOfArgs) {
            // 获得的实参个数 小于 原函数的形参个数,则继续调用 bind()
            return fn.bind(null, ...rest)
        } else {
            // 已经得到了足够的实参,则执行原函数
            return targetfn.apply(null, rest)
        }
    }
}

function add(a, b, c, d) {
    return a + b + c + d
}

// 柯里化:将一个多参数函数转化为多个嵌套的单参数函数
curry(add)(1)(2)(3)(4)  // 10

反柯里化

作用:在于扩大函数的适用性,使本来作为特定对象所拥有的功能的函数可以被任意对象所用.

对反柯里化更通俗的解释可以是 函数的借用,是函数能够接受处理其他对象,通过借用泛化、扩大了函数的使用范围。

放一个通用的柯里化函数:

var uncurrying= function (fn) {
    return function () {
        var args=[].slice.call(arguments,1);
        return fn.apply(arguments[0],args);        
    }    
};
上一篇下一篇

猜你喜欢

热点阅读