Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

js-函数柯里化 #12

Open
ahaow opened this issue May 9, 2022 · 0 comments
Open

js-函数柯里化 #12

ahaow opened this issue May 9, 2022 · 0 comments

Comments

@ahaow
Copy link
Owner

ahaow commented May 9, 2022

柯里化

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

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

// 执行add函数
add(1,2)

// 假设有一个curry函数可以做到柯里化
let curry_add = curry(add)
curry_add(1)(2)

用途

function ajax(type, url, data) {
  let xhr = new XMLHttpRequest()
  xhr.open(type, url, true)
  xhr.send(data)
}

ajax('POST', 'www.test.com', 'name=ahao')
ajax('POST', 'www.test2.com', 'name=ahao')
ajax('POST', 'www.test3.com', 'name=ahao')

// 利用curry
let ajaxCurry = curry(ajax)

// 以 POST 类型请求数据
let post = ajaxCurry('POST')
post('www.test.com', 'name=ahao')

// 以 POST 类型请求来自于 www.test.com 的数据
let postFromTest = post('www.test.com')
postFromTest("name=kevin");

// curry这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性

实现原理

「用闭包把传入的参数保存起来,当传入的参数的数量足够执行函数时,就开始执行函数」

第一版

function curry(fn) {
  let args = [].slice.call(arguments, 1)
  return function() {
    let newArgs = args.concat([].slice.call(arguments))
    fn.apply(this, newArgs)
  }
}

function add(a, b) {
  return a + b
}
let addCurry1 = curry(add, 1, 2)
let addCurry2 = curry(add, 1)
let addCurry3 = curry(add)
console.log(addCurry1())
console.log(addCurry2(4))
console.log(addCurry3(1,5))

已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。

第二版

参考文档:[[JavaScript专题之函数柯里化](https://github.com/mqyqingfeng/Blog/issues/42)](mqyqingfeng/Blog#42)

function sub_curry(fn) {
  let args = [].slice.call(arguments, 1)
  return function() {
    return fn.apply(args.concat([].slice.call(arguments)))
  }
}

function curry(fn, length) {
  length = length || fn.length
  let slice = Array.prototype.slice
  return function () {
    if (arguments.length < length) {
      let combined = [fn].concat(slice.call(arguments))
      return curry(sub_curry.apply(this, combined), length - arguments.length)
    } else {
      return fn.apply(this, curry)
    }
  }
}
function add(a,b,c) {
  return [a, b, c];
}
var fn = curry(add);
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"]

第三版

参考文档:yygmind/blog#37

function currying(fn, length) {
  // 1. 第一次调用获取函数fn参数的长度,后续调用获取fn剩余参数的长度
  length = length || fn.length
  // 2. currying 包裹之后返回一个新函数,接收参数为 ...args
  return function(...args) {
    // 3. 新函数接收的长度是否大于fn剩余参数需要接受的长度
    if (args.length >= length) {
      // 4. 满足要求,执行函数fn,传入新函数的参数
      fn.apply(this, args)
    } else {
      // 5. 不满足要求,递归currying函数,新的fn为bind返回的新函数(bind绑定了`...args`参数,未执行),新的length为fn剩余参数的长度
      currying(fn.bind(this, ...args), length - args.length)
    }
  }
}

// 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"]

解析

add(1, 2, 3) // 6
function add(a) {
  return function(b) {
    return function(c) {
      return a + b + c
    }
  }
}

前面两次调用每次返回一个新函数,第三次调用后满足参数长度要求然后执行函数

再来看 currying 实现函数,其实就是判断当前参数长度够不够,参数够了就立马执行,不够就返回一个新函数,这个新函数并不执行,并且通过 bind 或者闭包保存之前传入的参数

扩展:函数参数 length

函数currying的实现中,使用了fn.length来表示函数参数的个数,那么fn.length表示函数的所有参数个数吗? 并不是

函数的length属性获取的是形参的个数,但是形参的数量不包括剩余参数的个数,而且仅包括第一个具有默认值之前的参数的个数

((a, b, c) => {}).length // 3

((a, b, c = 3) => {}).length // 2

((a, b = 3, c) => {}).length // 1

((a = 1, b, c) => {}).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: 获取的是形参的个数,但是形参的数量不包括剩余参数的个数,而且仅包括第一个具有默认值之前的参数个数
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant