likes
comments
collection
share

📕 JavaScript冷饭系列:闭包,什么闭包?

作者站长头像
站长
· 阅读数 67

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️

人家写“TCP三次握手”都那么多赞,我不管我也要,嘤嘤嘤

如果你用一个循环来设置多个 setTimeout 会发生什么呢?比如说,你想让它们分别在 1 秒、2 秒、3 秒…之后执行,并且打印出对应的数字🔢。你可能会写出这样的代码:

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

你觉得这样会打印出什么呢?在解答这个题目之前,我们先学习一下我们今天的主题❤

❓什么是闭包?

闭包是指一个函数能够访问另一个函数作用域中的变量的函数。闭包可以让开发者从内部函数访问外部函数的作用域。

这个定义来自于《红宝书》第178页。

❓为什么会产生闭包?

针对这个问题我们需要举一个简单的函数例子在整个JavaScript中是如何运行的:

function template(num) {
  if (num) {
    const temp = num;
    console.log(temp);
  }
}
template();

JavaScript 的执行过程可以分为以下几个步骤,然后才真正开始运行代码:

语法分析
语义分析
JS源代码
tokens
AST
字节码
开始运行程序
  • JS 源代码经过语法分析,转化成 tokens
  • tokens 经过语义分析,转化为 AST(抽象语法树)
  • 抽象语法树会被转化为字节码
  • JS 运行时开始运行这段上面生成代码

如果你想更好地理解这个过程,可以把它想象成一个人在读一篇文章。首先,他会把文章分成很多小段,也就是 tokens。然后,他会把这些小段组合起来,形成一个完整的句子或者段落,也就是 AST。最后,他会理解这个句子或者段落的意思,并且可以用自己的话来表达出来,也就是字节码。

当函数运行时,会执行以下步骤:

  1. 函数声明:当代码执行到函数声明的时候,JavaScript会询问作用域链,看看是否已经声明了 template 函数。如果没有声明,就会在当前作用域中创建一个 template 函数(这里template是没有声明的,所以会在全局作用域中创建)。

console.log 中的 console内置对象,虽然不是我们声明的,但是它已经在全局作用域了。

  1. 执行 templateJavaScript同样会询问作用域链,看看是否已经声明了 template 函数。如果没有声明,就会报Reference Error

  2. 进入到template函数中:代码进入到了 template 函数中。我们创建了一个新的作用域,并将其指向全局作用域,从而形成了一个新的作用域链

  3. 检查 num 变量JavaScript同样会询问作用域链,看看是否已经声明了 num 变量。在 template 函数中的新的作用域中找不到 num 变量时,它就会沿着链向上查找(如果当前作用域找到就返回),最终都找不到时就会报Reference Error。这个过程类似于原型链

  4. ……

剩下的部分我就不再解释了,我懒得写相信你应该能够理解。

实际上,作用域背后的原理是词法环境。词法环境由两部分组成:

  1. 环境记录:这其实就是 JavaScript 用来存储变量的地方,一个 key-value 对在这里被称为一个 binding
  2. 外部环境的引用。

全局作用域,总是出现在作用域的最外层。全局作用域对应的环境就是全局环境,全局作用域的外部环境引用是 null。

由此,我们发现由于 Javascript 的解析逻辑,就会产生作用域链。当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中

每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    console.log(x + y + z); // 60
  }
  bar();
}
foo();

由此,闭包产生的本质就是,当前环境中存在指向父级作用域的引用

❓闭包有哪些表现形式?

明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?

  1. 返回一个函数:

    // 定义一个函数,接受一个参数 x,并返回一个新的函数
    function makeAdder(x) {
      // 返回一个匿名函数,该函数接受一个参数 y,并返回 x + y
      return function(y) {
        return x + y;
      };
    }
    
    // 调用 makeAdder() 函数,并传入 5 作为参数,得到一个新的函数 add5
    var add5 = makeAdder(5);
    
    // 调用 add5() 函数,并传入 2 作为参数,得到结果 7
    var result = add5(2);
    
    // 输出结果
    console.log(result); // 7
    
    
  2. 把整个函数作为参数传入:

    // 定义一个比较函数,按照字符串长度升序排列
    function compareByLength(a, b) {
      return a.length - b.length;
    }
    
    // 创建一个字符串数组
    let fruits = ["apple", "banana", "cherry", "durian", "elderberry"];
    
    // 调用 sort() 方法,并传入比较函数作为参数
    // 这就是闭包
    fruits.sort(compareByLength);
    
    // 输出排序后的数组
    console.log(fruits); // ["apple", "banana", "cherry", "durian", "elderberry"]
    
    
  3. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

    // 定时器
    setTimeout(function timeHandler(){
      console.log('111');
    },100)
    
  4. IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window当前函数的作用域,因此可以全局的变量。

    var x = 2;
    (function IIFE(){
      // 输出2
      console.log(x)
    })();
    

❓思考:为什么不能像开头那样写?

我们回到文章首页那道题:

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

你觉得这样会打印出什么呢?1、2、3、4、5 吗?不好意思,答错了。实际上,这样会打印出 6、6、6、6、6 。为什么呢?因为当你的 setTimeout 轮到执行的时候,循环已经结束了,i 的值已经变成了 6。

而且,用的是 var 来声明 i,这样就把 i 变成了全局变量🌎。

所以,当你的 setTimeout 里面的函数想要找 i 的时候,它就会去全局找,发现了 i 是 6,就打印出来了😂。

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)

因为setTimeout为宏任务,由于JavaScript 中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。就像是一条单行道,一次只能走一个车🚗。所以,setTimeout 就像是一个耐心的司机,它会把自己的车停在路边,等到前面的车都走完了,才会开上去🚙。

解决方法:

1、利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中

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

2、给定时器传入第三个参数, 作为timer函数的第一个函数参数

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

3、使用ES6中的let

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

🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨

转载自:https://juejin.cn/post/7243019785099984955
评论
请登录