简单了解柯里化

84 阅读2分钟

1.闭包

  • 一般情况下定义在函数内部的变量在函数外部是不可访问的。但某些时候有又确实有这样的需求,这时就会用到闭包。
  • 闭包概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域 。(能够读取其他函数内部变量的函数)通过闭包我们可以在一个函数内部访问另一个函数内部的变量。
  • 简单理解:闭包 = 内层函数 + 引用的外层函数变量
1.闭包的形式

下面介绍闭包的形式,也就是访问函数内部变量的常见手段。

1 函数返回值为函数

function foo(){
    let name = 'xiaom'
    return function (){
        return name
    }
}
const bar = foo()
console.log(bar())
// 全局变量bar获取到了局部作用域foo的内部变量,这是最常见的形式

2 内部函数赋给外部变量

let num;
function foo(){
  const _num = 18;
  function bar() {
    return _num;
  }
  num = bar;
}
foo();
console.log(num()); // 18

3 通过立即执行函数行成独立作用域,保存变量(es6之后使用let,const替代)

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}
// 上述代码的预期输出结果是每隔一秒按顺序输出12345。
// 定时器任务会在同步任务执行完毕后再执行。因此此时的i已经变成6 会直接输出5个6。
// 解决这一问题的关键就是每次循环形成一个独立作用域,这样定时器中的操作执行时会访问对应作用域的变量。
for (var i = 1; i <= 5; i++) {
  //  包一层立即执行函数  并传入i  由于内部操作用到了i 因此会形成闭包
  (function (i) {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  })(i);
}

闭包函数的其他形式大多是以上形式的变体。

2.闭包的优缺点
- 1.外部可以访问函数内部变量。
- 2.让函数内部变量一直保留在内存中。
function fn1() {
  let count = 0;
  function fn2() {
    count++;
    return count;
  }
  return fn2;
}
let result1 = fn1()
console.log(result1()) // 1
console.log(result1()) // 2
//通常来讲,函数执行完毕后,函数连同它内部的变量会被一同销毁。
//由于函数fn内部变量count被外部引用,因此fn执行完毕后,其内部变量count不会被销毁。因此过度使用闭包会造成内存消耗。
- 3.形成独立作用域。

显然,通过上述第二点也能看出,由于闭包会使函数内部变量一直保存在内存中,造成内存消耗,因此过度使用会造成页面性能问题。解决方法是及时删除不使用的局部变量。

2.高阶函数

闭包的一个典型应用:柯里化函数 。介绍柯里化之前需要先了解 高阶函数 的概念。

定义:接收函数作为参数或者返回函数的函数

简单理解就是:

  1. 首先是个函数
  2. 参数或者返回值是函数

举例子

我们这里举两个例子来覆盖下上文的定义,其中,例一为接收函数作为参数的高阶函数,例二为返回函数的高阶函数。

例一:函数作为参数

我们定义了一个叫evaluatesToFive的函数,接收两个参数:第一个参数是一个数字,第二个参数是一个函数。在函数evaluatesToFive中,将参数一(数字)传入参数二(函数)

function evaluatesToFive(num, fn) {
  return fn(num) === 5;
}

使用的场景:

function divideByTwo(num) {
  return num / 2;
}

evaluatesToFive(10, divideByTwo);
// true

evaluatesToFive(20, divideByTwo);
// false

例二:返回函数

创建一个函数multiplyBy,接收一个数字作为参数,并返回一个新的函数

function multiplyBy(num1) {
  return function(num2) {
    return num1 * num2;
  };
}

使用场景:

const multiplyByThree = multiplyBy(3);
const multiplyByFive = multiplyBy(5);

multipyByThree(10); // 30

multiplyByFive(10); // 50

主要是通过生成新的函数以达到更具体的目的

复杂一点的应用实例

本例中,我们创建一个函数去检测新用户的注册信息是否能通过检验规则:

  • 大于18岁
  • 密码长度大于8
  • 同意用户协议

新用户的注册信息大概是这样:

const newUser = {
  age: 24,
  password: 'some long password',
  agreeToTerms: true,
};

接下来,我们来创建三个验证函数,通过返回true,否则返回false

function oldEnough(user) {
  return user.age >= 18;
}

function passwordLongEnough(user) {
  return user.password.length >= 8;
}

function agreeToTerms(user) {
  return user.agreeToTerms === true;
}

接下来,该主角登场了,我们需要创建一个高阶函数来一次性完成所有的验证。参数一是新用户注册信息,剩下的参数是我们上文创建的三个验证函数。在函数体中依次执行验证:

function validate(obj, ...tests) {
  for (let i = 0; i < tests.length; i++) {
    if (tests[i](obj) === false) {
      return false;
    }
  }
  return true;
}

使用:

const newUser1 = {
  age: 40,
  password: 'tncy4ty49r2mrx',
  agreeToTerms: true,
};

validate(newUser1, oldEnough, passwordLongEnough, agreeToTerms);
// true

const newUser2 = {
  age: 40,
  password: 'short',
  agreeToTerms: true,
};

validate(newUser2, oldEnough, passwordLongEnough, agreeToTerms);
// false

3.柯里化函数

柯里化就是一种特殊的高阶函数,下面介绍柯里化函数的概念。

定义: 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

这里以数字相加为例子做简单说明:

我们通常是这样写一个函数来求得 两数相加 的值:

function sum(a,b){
    console.log(a+b)
}
sum(1,2)

没有问题,然后需求变成了:三数相加

修改后变成这样:

function sum(a,b,c){
    console.log(a+b+c)
}
sum(1,2,3)

问:这样写,有毛病吗?? 有!

这样一改,既违反了:“开闭原则”、又违反了:“单一职责原则”。

相关解释:

  • 什么是“开闭原则”?即:我们编程中要尽可能的避免直接修改函数、类或模块,而是要在原有基础上拓展它;
  • 什么是“单一职责原则”?即:每个函数、类或模块,应该只负责一个单一的功能;

首先,我们修改了 sum 函数的传参以及内部的调用 ⇒ 则违反“开闭原则”

其次,sum 函数本来只负责两数相加,修改后,它又负责三数相加,职责已经发生了变化 ⇒ 则违反 “单一职责原则”;

如果正规按照单一责任来写,应该是:

// 负责两数相加
function sum2(a,b){
    console.log(a+b)
}

// 负责三数相加
function sum3(a,b,c){
    console.log(a+b+c)
}

事实上,是不可能这样去写的,因为如果有一万个数相加,得写一万个函数。

而 加法只有一个!!  不管你最终要加几个值,总是要一个加一个。

于是乎,我们设想,能不能写一个这样的函数:它的功能,就是“加”,参数跟几个,我就加几个。

// 负责“加法”,
function addCurry(){
    ...
    ...
    ...
}

addCurry(1)(2) // 两数相加
addCurry(1)(2)(3) // 三数相加
...
addCurry(1)(2)(3)...(n) // n 数相加

这种就是柯里化 为了能够实现一个加一个,即存储参数的目的,可以用到闭包(实现延迟处理)

举例解释如下:

function directHandle(a,b){
    console.log("直接处理",a,b)
}

directHandle(111,222)

// 直接处理 111 222

function delayHandle(a){
    return function(b){
         console.log("延迟处理",a,b)
    }
}

delayHandle(111)

//  ƒ (b){
//    console.log("延迟处理",a,b)
//  }

如上 delayHandle(111) 不像 directHandle(111,222) 直接打印值,而是先返回一个函数 f(b);111 也被临时保存了,delayHandle(111)(222),则得到相同的输出。这就是:延迟处理的思想。 所以这里我们借用闭包来实现最初版的柯里化:

// 两数相加
function addCurry(a){
    return function(b){
            console.log(a+b)
    }
}

addCurry(1)(2)

// 三数相加
function addCurry(a){
    return function(b){
        return function(c){
             console.log(a+b+c)
        }
    }
}

addCurry(1)(2)(3)

通过写这两个闭包的过程,可以看到有点像递归,当参数是 n 个的时候,需要递归 n-1 次 return function,所以addCurry可以写成这样:


 let arr = []
 function addCurry() {
     // `Array.prototype.slice.call()`方法能够将一个具有`length`属性的对象转换为数组
     let arg = Array.prototype.slice.call(arguments); // 递归获取后续参数
     arr = arr.concat(arg);
      if (arg.length === 0) { // 如果参数为空,则判断递归结束
          return arr.reduce((a,b)=>{return a+b}) // 求和
      } else {
          return addCurry;
      }
  }

addCurry(1)(2)(3)()

主要有3个作用: 参数复用提前返回延迟执行

用以上例子我们来简单地解释一下:
参数复用:拿上面 addCurry 这个函数举例,只要传入一个参数 z,执行,计算结果就是 1 + 2 + z 的结果,1 和 2 这两个参数就可以直接复用了。

提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。

以上就是柯里化的一种简单实践,待续......

参考

juejin.cn/post/710109…