[路飞]_看完这遍,妈妈再也不担心我不会闭包了

571 阅读8分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

什么是闭包(内功是什么)?

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

如何制作一个闭包(如何修炼内功)?

内部函数引用外部函数作用域中的变量就会形成闭包

例如:
 1: function a() {
 2:  let val = 1;
 3:  const b = function () {
 4:     val++;
 5:     return val;
 6:  }
 7:  return b;
 8: }
 9: let test = a();
10: let f1 = test();
11: let f2 = test();

接下来我们来看一下这段代码在javascript中是如何运行的,怎么就制作了一个闭包?

执行步骤:
  1. 在代码的第1-8行,在全局执行上下文中创建了一个新的变量,名为a,并且赋值了一个函数定义;
  2. 在代码的第9行,在全局执行上下文中声明了一个名为test的新变量;
  3. 在代码的第9行,这里调用了test函数,并且将其返回值赋值给了test变量;
  4. 在代码的第1-8行,调用这个函数,创建了一个新的本地执行上下文;
  5. 在代码的第2行,在本地执行上下文中,声明了一个名为val的新变量,并且赋值为1
  6. 在代码的第3-6行,声明了一个名为b的新变量,该变量在本地执行上下文中声明,变量的内容为第4行、第5行所定义的内容,我们还创建了一个闭包,并将其还作为函数定义的一部分,在这段代码中变量val的值为1
  7. 在代码的第7行,会返回变量b的内容,并且删除本地执行上下文,b变量和val变量不再存在,控制权交给了调用上下文,但是这里我们会返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量;
  8. 在代码第9行,在调用全局执行上下文中,a函数的返回值被赋值给test,变量test现在包含了一个定义的函数和闭包,这里由a返回的函数定义,它不再被标记为b,但是它的定义内容时相同的,在全局上下文中被称为test
  9. 在代码第10行,声明了一个新的变量f1
  10. 在代码第10行,查找变量test,然后发现它是一个函数,调用它,它的内容是前面返回的定义一个闭包
  11. 创建了一个新的执行上下文,没有参数;
  12. 在代码第4行,val++,寻找变量val,在我们查找本地和全局上下文之前,先让我们看一下闭包里面的内容,然后会发现,卧槽,竟然在闭包里面发现了一个名为val的变量,它的值为1,在经历完第4行代码之后,它的值设置为了2,它并且再次被存储在闭包里面,闭包里面现在包含了一个名为val并且值为2的变量;
  13. 在代码的第5行,返回了val的值,并且销毁了本地执行上下文;
  14. 回到代码的第10行,我们发现返回值2被赋值给了f1变量;
  15. 在代码第11行,会重复11-14步,这次闭包里面的变量值为2,通过第4行代码会被设置为3,并且再次存储在闭包里面,然后返回一个3f2被赋值为3,代码执行结束;

当函数返回一个函数的时候,闭包的概念就显得很重要了,被返回的函数可以访问不属于全局作用域的变量,但仅存在于闭包之中!

恍然大悟.jpeg

看到这里有没有一种恍然大悟的感脚,就好似原本乱七八糟的逻辑突然捋明白了....

继续?

各位看官老爷能看到这里,你大概已经明白闭包是什么了吧?在了解闭包是什么之后,我们再来看看惰性函数又是个啥子?

继续.jpeg

什么是惰性函数(小试牛刀)?

惰性函数表示函数执行的分支只会在函数第一次调用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。

使用背景

要求:实现一个a函数,a函数首次被调用的时候返回一个Date对象

方案一:
var time;
function a() {
  if (time) return time;
  time = new Date();
  return time;
}

在这个方案中,我们可以发现两个问题,第一个是time变量泄漏为全局变量,第二个是函数每次执行都要判断time的值,因为我们刚学过闭包,所以就有了方案二如下

方案二:
function a() {
  var time;
  return function () {
    if (time) return time;
    time = new Date();
    return time;
  }
}

方案二中我们可以得知,确实解决了time变量泄漏为全局变量这个问题,但是依然没有解决每次执行都要进行一个判断这个问题;接着我们闭关修炼研究方案三

闭关修炼.gif

方案三:
function a() {
  var time = new Date();
  return function () {
    return time;
  }
}

终于终于,泄漏为全局变量和重复判断的问题都解决了,解决的办法也很简单那就是重写函数,这也被称之为武林秘籍之 —— 惰性函数

闭包的应用有哪些(武林绝学)?

现在我们已经打好了基础,练好了内力,接来下我们就来学一些使用的招数

第一招:私有变量

JavaScript中的属性都是公有的,任何函数中定义的变量都可以认为是私有变量,因为我们在函数的外部无法访问这些变量。因此我们可以在函数内部创建一个闭包,闭包可以通过自己的作用域链来访问这些变量,所以我们就可以利用闭包,创建一个用于访问私有变量的公共方法。

例如:
function personal() {
  let name = 'Lilei';
  return {
    getName: funtion () {
      return name; 
    }
  }
}
​
let test = personal();
console.log(test.getName()); // Lilei
console.log(test.name); // undefined

从上面的方法中我们可以看出,personal函数内部的变量name只能通过内部的一个方法getName来访问,因此就构成了一个私有变量,我们可以通过一些特定的方法去访问这个变量,而用户不能随随便便的就访问到这个变量。至此我们就学会了第一招 —— 私有变量

第二招:柯里化 and 偏应用函数

柯里化

柯里化是把接受多个参数的函数变成接受一个单一参数的函数,返回并接受余下的参数,继返回结果的新函数的技术

如下一些普通的方法(花拳绣腿)

function getAddress(country, province, city) {
  console.log(country + '-' + province + '-' + city)
}
​
getAddress("中国", "山东", "菏泽");
getAddress("中国", "浙江", "杭州");

使用柯里化(招数)改造之后

function getAddress(country) {
  return function (province) {
    return function (city) {
      console.log(country + '-' + province + '-' + city)
    }
  }
}
​
let test = getAddress("中国");
test("山东")("菏泽");
test("浙江")("杭州");

我们可以发现,通过柯里化我们把它的部分参数进行了缓存(省掉了花拳绣腿,简化招式),使得函数使用起来更简单方便(招招致命)。

偏应用函数

偏应用函数是一个函数有多个参数,然后返回新函数,返回的函数接受剩余的参数完成函数的应用。

使用偏应用函数(招数)改造之后

function getAddress(country) {
  return function (province, city) {
    console.log(country + '-' + province + '-' + city)
  }
}
​
let test = getAddress("中国");
test("山东","菏泽");
test("浙江","杭州");

我们又可以发现,通过偏应用函数我们同样把部分参数进行了缓存(同样省掉了花拳绣腿,简化招式),也使得函数使用起来简单方便(同样招招致命)。

既然这两个招数都很厉害,那么究竟谁更胜一筹?

区别

通过比较我们发现它们都是使用了闭包的思想(内功),让函数能够保存一部分的参数,从而提高代码的复用;至此我们就又学会了第二招 —— 柯里化 and 偏应用函数

第三招:即时函数

即时函数也称为立即执行函数,定义后立即执行的函数

特点(诀窍):

  1. 函数执行后销毁,内部变量也会被回收,在我们不想保留一个全局变量和避免命名冲突的时候可以使用即时函数;
  2. 即时函数可以配合闭包来使用,来开辟一个独立的作用域,内部的变量可以用来保存数据,而且外部无法访问到这个变量;
(function() {
  let val = 0;
  document.getElementById('btn').onClick = function () {
    val++;
    console.log('val:', val);
  }
})()

编译此代码函数就会立即执行,内部的变量也会被回收,但是因为内部函数引用了一个外部函数的变量,所以val不能销毁,当我们调用IDbtn的一个按钮时,val变量就会做一个自增,切外部无法访问到这个变量;至此我们就又学会了第三招 —— 即时函数

缺点

  1. 常驻内存,增加内存的使用量;
  2. 使用不当会照成内存泄漏;

最后如果你对本文有任何的建议或者补充,可以在留言区留言,我会认真阅读每一位读者提出的有效的建议!感谢阅读,记得点赞,Thanks~