JS面试高频题: 柯里化函数的实现

517 阅读6分钟
  • 在维基百科和百度百科中,对柯里化的定义:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术
  • 作用和特点:参数复用,提前返回,延迟执行
  • 本来是想直接上面试题的,但是感觉还是有一些涉及的知识值得讲一讲

一、知识补充

1、实参和形参个数

  • 实参的数量可以通过函数中arguments对象的length属性获得
  • 形参的数量可以由函数的length属性获得
function add (a,b) {
  console.log(arguments.length)
  return a+b
}
console.log(add.length) //2,形参数量有2个
add(1,2,3,4) //4,实参数量有4个

2、剩余参数

  • 剩余参数语法允许我们将一个不定数量的参数表示为一个数组

  • arguments的区别:

    • 剩余参数只包含那些没有对应形参的实参,而agruments是包含所有参数的
    function add (a,b,...args) {
     console.log(args.length)  //3 ,剩余的参数有3个
     console.log(arguments.length)//5,实参数量有5个
     return a+b
    }
    add(1,2,3,4,5) 
    
    • arguments是一个伪数组,而剩余参数是真正的Array实例
    function add (a,b,...args) { 
    //通过instanceof搜索原型链比较
      console.log(arguments instanceof Array) //false
      console.log(args instanceof Array) //true
      return a+b
    }
    add(1,2,3,4,5) 
    

3、将伪数组转换为真实数组

  • 通过遍历伪数组然后push进新数组,返回新数组
    function add (a,b,...args) {
      console.log(arguments instanceof Array) //false
      let newArr = [];
      for(let i = 0;i < arguments.length;i++) {
        newArr.push(arguments[i])
      }
      console.log(newArr instanceof Array) //true
      return a+b
    }
  • 使用数组原型上的slice()方法,也可以继续传入参数进行切割
function add (a,b,...args) {
  console.log(arguments instanceof Array) //false
  let newArr = Array.prototype.slice.call(arguments)
  console.log(newArr instanceof Array) //true
  return a+b
}
  • 使用ES6的Array.from()方法,这个方法会更加灵活,第二个参数可以放入函数类似map遍历,第三个函数可以改变this的指向
function add (a,b,...args) {
  console.log(arguments instanceof Array) //false
  let newArr = Array.from(arguments);
  console.log(newArr instanceof Array) //true
  return a+b
}

4、reduce方法

  • 对数组中的每个元素执行一个由我们提供的reducer函数(升序执行),将其结果汇总为单个返回值;不会改变原数组
  • 第一个参数为回调函数,可以传入四个参数
    • total(必须):initalValue,或者是函数先前返回的值
    • currentValue(必须):当前元素的值
    • index:当前元素的数组索引
    • arr: 当前元素所属的数组对象
  • 第二个参数为initalValue: 作为初始值传递给函数的值
const arr = [1,2,3,4];
const res =  arr.reduce((a,b)=>{
  return a+b; //每次返回的是函数先前返回的值/initalValue + 当前的元素值
},0)
console.log(res)

5、闭包

  • 为什么讲闭包呢,其实柯里化函数就是闭包的一种经典用法,要学会柯里化函数当然要先搞明白什么是闭包
  • 首先要知道JavasSript 语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量
  • 闭包: 能够访问其他函数内部变量的函数
  • 由于在 JavasSript 中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成"定义在一个函数内部的函数"
  • 在本质上,闭包是将函数内部和函数外部连接起来的桥梁

6、call,apply,bind

1.apply
  • 接收两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入
  • 当第一个参数为null,undefined的时候,默认指向window
  • 改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
2.call
  • call方法的第一个参数也是this的指向,后面传入的是一个参数列表
  • 改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
  • 当第一个参数为nullundefined的时候,默认指向window(在浏览器中)
3.bind
  • 第一参数也是this的指向,后面传入的也是一个参数列表
  • bind可以分为多次传入
  • bind是返回绑定this之后的函数

面试题一、实现参数定长的柯里化函数

  • 实现一个add函数,使其能够满足预期 add(1)(2)(3)=6
  • 先来一个“粗鲁” 的版本:使用闭包层层嵌套
  • 缺点:很不优雅,因为是层层嵌套,如果是n个数相加,那么则需要嵌套n层, 代码过于累赘
const add = function(a) {
  return function(b) {
    return function(c) {
      return a+b+c
    }
  }
}
console.log(add(1)(2)(3))
  • 既然不想要粗鲁的版本,那我们不如转化一下问题,把问题变为
  • 实现一个函数功能:add(1,2,3,4…n)转化为 add(1)(2)(3)(4)…(n),这样的话无论是需要几个数相加,都可以直接通过该函数转化
  • 我们来逐步实现该函数
    • 首先应该我们能想到的应该是先获取函数多次调用时传入的参数
    function curry(fn) {
      return function () {
        const restArgs = Array.from(arguments);
        console.log(restArgs)
        return curry(fn)
      }
     }
     function add(a,b,c,d) {
       console.log(a+b+c+d);
     }
    const addCurry = curry(add);
    addCurry(1)(2)(3)(4)
    //控制台打印
    //1
    //2
    //3
    //4
    
    • 控制台是多次输出了我们想要的参数,但是我们最后使用add函数时想要的应该是参数列表或者参数数组,而不是零散的参数
    • 那我们应该每次都记录下传递进来的参数,然后在再次调用函数的时候传入并合并所有参数
    function curry(fn) {
    //这里是需要去除第一个参数,因为我们第一个参数传入的是函数,并非我们预期需要的参数
     const presetArgs = Array.prototype.slice.call(arguments,1);
     console.log(presetArgs)
     return function () {
       const restArgs = Array.from(arguments);
       const allArgs = [...presetArgs,...restArgs];
       return curry(fn,...allArgs)
     }
    }
    function add(a,b,c,d) {
     console.log(a+b+c+d);
    }
    const addCurry = curry(add);
    addCurry(1)(2)(3)(4);
    控制台打印:
    //1
    //1,2
    //1,2,3
    //1,2,3,4
    
    • 获取到期望的参数后其实基本框架就出来了,只需要加个判断即可
      • 已经获取到传入的函数所需传入的形参个数,那么就直接调用传入函数
      • 否则继续柯里化
    function curry(fn) {
      const argLen = fn.length;
      const presetArgs = Array.prototype.slice.call(arguments,1);
      return function () {
        const restArgs = Array.from(arguments);
        const allArgs = [...presetArgs,...restArgs];
        if(allArgs.length >= argLen) {
          return fn.call(this, ...allArgs)
        } else {
          return curry(fn,...allArgs)
        }
      }
    }
    
    • 使用箭头函数+三目运算符简化一下就剩两行代码咯
    • 理解了上面的思路再来看这一串应该就没问题啦
    function curry(fn) {
      var c = (...arg) => (fn.length === arg.length) ? fn(...arg) : (...arg1) => c(...arg,...arg1)
      return c
    }
    
  • 测试该函数
function add(a,b,c,d) {
 console.log(a+b+c+d);
}
const addCurry = curry(add);
addCurry(1,2)(3)(4); //10
addCurry(1)(2)(3)(4); //10
addCurry(1,2,3,4); //10
addCurry(1)(2,3)(4); //10

实现了这个你会感觉不得劲哇,虽然传参数的方式自由多了,少传入一个参数就得不到结果,多传一个就报错

别急,继续往下看

面试题二:不定长的柯里化函数

  • 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6; 
add(2, 6)(1) = 9; 
add(1, 2, 3)(4) = 10; 
add(1)(2)(3)(4)(5) = 15;
  • 先按照我们上一道题的思路,先获取到所有参数
function add() {
  const presetArgs = Array.from(arguments);
  const _add = function() {
    const resetArgs = Array.from(arguments);
    const allArgs = [...presetArgs,...resetArgs]
    return add.apply(null,allArgs)
  }
  console.log(presetArgs)
  return _add
}
add(1)(2)(3,4)
//控制台打印
//1
//1,2
//1,2,3,4
  • 其实就是该函数既要能够不断调用,也要能够满足需要打印时能够输出想要的结果,那么可以考虑修改然后函数的toString方法(原因:使用console.log()打印函数时,实际上打印的是函数调用toString之后的结果)
  • 这样就可以让他在调用过程中既能返回函数进行递归调用又可以通过console.log()打印获得我们需要的结果
function add() {
  const presetArgs = Array.from(arguments);
  const _add = function() {
    const resetArgs = Array.from(arguments);
    const allArgs = [...presetArgs,...resetArgs]
    return add.apply(null,allArgs)
  }
  _add.toString = function() {
    return presetArgs.reduce((a,b)=>{
      return a+b
    },0)
  }
  return _add
}
  • 测试函数
console.log(add(1)(2)) //3
console.log(add(1)(2)(3)(4)) //10
console.log(add(1)(2)(3,4,5)) //15
console.log(add(1)(2,3)(4,5)(6)) //21

对柯里化函数的学习就到这里啦,通过对柯里化的学习顺便也回顾了一下涉及的其他知识