面试知多少?--柯里化(颗粒化)

147 阅读4分钟

一、柯里化的基本概念

柯里化的基本操作是将一个多参数函数转换为一系列接受单一参数的函数。这种做法带来了两个明显的好处:

  1. 函数粒度更小:每个函数只关心一个参数,逻辑更加简洁且易于理解。
  2. 参数复用:通过分步骤传递参数,可以使得一些通用的函数组件更容易复用,提升代码的适用性。

但同时还有一个缺点:柯里化虽然能提升复用性,但也导致了通用性降低。

举个简单例子:

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

//柯里化后
add(1)(2)(3)  //6

这个函数 add 接受三个参数。如果我们使用柯里化,它就会变成一个函数链,每个函数只接受一个参数。

二、柯里化应用示例一:简单感受一下柯里化的复用性提升

柯里化可以帮助分步传递参数,提升函数的复用性。假设我们有一个 ajax 请求函数,我们可以将其柯里化,使得指定请求类型(如 POST)的部分变得复用,而不必每次都传递相同的参数。

function ajax(type, url, data) {
  let xhr = new XMLHttpRequest();
  xhr.open(type, url, true);
  xhr.send(data);
  xhr.onreadystatechange = function () {
    // 处理响应
  };
}

let ajaxCurry = curry(ajax);
let post = ajaxCurry('POST');  // 只传递 POST 方法

post('www.test.com', 'name=awei');  // 执行 AJAX 请求

在这个示例中,我们使用柯里化使得 POST 请求类型复用,而每次调用时只需要传递 URL 和数据。大大降低了参数的冗余。

三、柯里化应用示例二:提取某一属性

柯里化的另一个常见应用是在数组的处理上。通过柯里化函数,我们可以实现更简洁、更灵活的数据映射操作。例如,假设我们有一个数组 person,包含多个对象,我们想要提取每个对象的 name 属性。可以使用柯里化的 prop 函数来简化这一操作:

let person = [
  { name: 'zhangsan', age: 18, sex: 'man' },
  { name: 'lisi', age: 20, sex: 'man' }
];

let prop = curry((key, obj) => obj[key]);  // 希望有一个柯里化函数处理(key, obj) => obj[key](还没写)

const newProp = prop('name')  //传入第一个参数key

let names = person.map(newProp) 

//此时如果我们还有一个含name属性数组

let arr = [
  {name: '张三'},
  {name: '李四'},
  {name: '王五'}
]

let newArr = arr.map(newProp)  

// 任何数组调用 map 方法,都可以通过触发 newProp 方法,
// 来获取到数组中每个对象的 name 属性值

在这里,prop 函数接受一个键名作为第一个参数,返回一个可以提取对象指定属性的函数。通过柯里化,我们在 map 操作中只需传递 newProp,即可灵活处理每个对象,提取每个对象的 name 属性。

四、如何实现柯里化

我们首先需要了解如何实现一个柯里化函数。柯里化的关键是通过递归返回一个新函数,直到所有参数都被传入,最后执行原始的函数逻辑。

以下是一个柯里化函数的简单实现:

function curry(fn, ...args) {
  let length = fn.length;  // 获取原函数的参数个数
  args = args || [];  // 默认的参数为空数组

  return function () {
    let _args = args.slice(0), arg;
    for (let i = 0; i < arguments.length; i++) {
      arg = arguments[i];
      _args.push(arg);  // 将新传入的参数合并
    }

    // 如果已传入的参数个数小于原函数的参数个数,继续返回一个新的函数
    if (_args.length < length) {
      return curry(fn, ..._args);
    } else {
      // 参数齐全,执行原函数
      return fn(..._args);
    }
  }
}

这里,curry 函数接受一个函数和一组参数,通过递归的方式将参数传递给原函数,直到参数个数满足要求时,执行原函数。

五、字节面试题:累加函数

字节有过这样一道面试题,手写一个sum函数,实现以下输出,考的就是柯里化。

//function sum(){
//  //实现以下输出
//}

// 实现代码
function sum(...args) {
  let _args = args;
  return function () {
    if (arguments.length === 0) {  // 当没有新参数时,计算结果
      return _args.reduce((pre, item) => pre + item, 0);
    } else {
      _args = [..._args, ...arguments];  // 将新传入的参数加入
      return sum(..._args);  // 递归调用继续累加
    }
  }
}

console.log(sum(1, 2)());           // 输出 3
console.log(sum(1, 2, 3)(4, 5)());  // 输出 15
console.log(sum(1, 2, 3)(4, 5)(6)());  // 输出 21

这个例子中,sum 函数会递归地接收参数,直到没有新参数传入为止,然后返回累计的结果。