JS-作用域闭包

256 阅读2分钟

对于那些有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力才能理解这个概念

闭包就像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里

秘诀:JavaScript中闭包无处不在,你只需要能够识别并拥抱它

来看一段代码

function foo(){
  var a = 2;
  function bar(){
    console.log(a);
  }
  return bar;
}

var baz = foo();
bar(); // 2

这里bar()可以访问foo()的内部作用域,我们将bar()函数本身当作一个值类型进行传递。

foo()执行后,返回值赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()

bar()依然持有对该作用域的引用,而这个引用就叫做闭包

无论通过何种手段将内部函数传递到所在的词法作用域以外,他都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

本质上无论何时何地,如果将函数(访问他们各自的词法作用域)当作第一级的值类型并到处传递,你都会看到闭包在这些函数中的应用,在定时器,事件监听器,ajax请求,跨窗口通信,WebWorkers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包

循环与闭包

接下来的几个例子,我希望你能理解并能给别人讲明白

for(var i = 1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  },i*1000);
}

正常情况下,对这段代码的预期是分别输出数字1-5,每秒一个,每次一个

但是,这段代码的实际运行效果是输出**==5个6==**

6的来源肯定是for循环的结束条件产生的,i<=5退出的条件就是i=6

但是为什么是6呢?难道每个循环的i都不会保存吗?

  1. 循环中所有回调函数都会在循环结束时才执行,也就是setTimeout会在for结束时才执行
  2. for(var i = 1;i<=5;i++)i只有一个,因为他们都被封闭在一个共享的全局作用域中,所以用的都是同一个i

那,是不是我们给每个循环都单独开一个作用域就行了?

for(var i = 1;i<=5;i++){
  (function(){
    setTimeout(function timer(){
      console.log(i);
    },i*1000);
  })();
}

看似我们通过IIFE创建了一个作用域,但是还是没用,结果依然输出了==5个6==

因为我们都IIFE只是一个什么都没有的空作用域,它需要包含实质内容才能为我们所用

for(var i = 1;i<=5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j);
    },i*1000);
  })(i);
}

这里我们把i作为参数给了我们的IIFE函数,好像,没问题了,输出是1-5。

因为在迭代中使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问,也就是变量j

不用IIFE有没有其他办法可以实现相同的效果?

我们先来看IIFE给我们解决了什么问题?

  • 新建了一个单独的作用域,存放了正确的变量值

也没有其他方法可以实现相同的功能?

  • 有,let!!!
for(let i = 1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  },i*1000)
}

上面的代码会输出1-5,和我们预想的效果一致(let大法好)

因为let会创建一个块作用域与当前的迭代循环中,可以理解为下面的代码

for (var i = 1;i<=5;i++){
  let j = i;
  setTimeout(() => {
    console.log(j);
  }, j*1000);
}

非常酷,非常快乐

有读者可能会想,我把let换成var行不行?

for (var i = 1;i<=5;i++){
  var j = i;
  setTimeout(() => {
    console.log(j);
  }, j*1000);
}

我在每个for循环中都定了一个变量j,看起来好像没啥问题?

好,运行

==5个5==

新的从未出现过的结果!

结果并非如我们所料般,全都输出了5,因为j被重复定义了,我们再来加一行代码,你也许就能明白为什么了

for (var i = 1;i<=5;i++){
  var j = i;
  setTimeout(() => {
    console.log(j);
  }, j*1000);
}
console.log(j); // new add

我们在结尾打印了j,成功输出了5,这个时候我们终于意识到了

  • 噢,j是定义在全局作用域里的,所以它被重复定义了

具体的let和var的区别,可以看这篇文章👉juejin.cn/post/692564…