📕 重学JavaScript:你理解闭包吗?

简介: 闭包是一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。

📕 重学JavaScript:你理解闭包吗?

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

如果你用一个循环来设置多个 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 的执行过程可以分为以下几个步骤,然后才真正开始运行代码:

graph LR;
    A[JS源代码] --语法分析--> B[tokens];
    B --语义分析--> C[AST];
    C --> D[字节码];
    D --> E[开始运行程序];
  • JS 源代码经过语法分析,转化成 tokens
  • tokens 经过语义分析,转化为 AST(抽象语法树)
  • 抽象语法树会被转化为字节码
  • JS 运行时开始运行这段上面生成代码

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

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

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

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

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

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

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

  5. ……

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

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

  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)
    
  1. 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);
}

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

目录
相关文章
|
30天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
15天前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
104 58
|
15天前
|
缓存 JavaScript 前端开发
js 闭包
【10月更文挑战第27天】JavaScript闭包是一种强大的特性,它可以用于实现数据隐藏、记忆和缓存等功能,但在使用时也需要注意内存泄漏和变量共享等问题,以确保代码的质量和性能。
34 7
|
17天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包:解锁编程潜能,释放你的创造力
【10月更文挑战第25天】本文深入探讨了JavaScript中的闭包,包括其基本概念、创建方法和实践应用。闭包允许函数访问其定义时的作用域链,常用于数据封装、函数柯里化和模块化编程。文章还提供了闭包的最佳实践,帮助读者更好地理解和使用这一强大特性。
12 2
|
1月前
|
设计模式 JavaScript 前端开发
探索JavaScript中的闭包:从基础概念到实际应用
在本文中,我们将深入探讨JavaScript中的一个重要概念——闭包。闭包是一种强大的编程工具,它允许函数记住并访问其所在作用域的变量,即使该函数在其作用域之外被调用。通过详细解析闭包的定义、创建方法以及实际应用场景,本文旨在帮助读者不仅理解闭包的理论概念,还能在实际开发中灵活运用这一技巧。
|
1月前
|
缓存 JavaScript 前端开发
深入了解JavaScript的闭包:概念与应用
【10月更文挑战第8天】深入了解JavaScript的闭包:概念与应用
|
1月前
|
自然语言处理 JavaScript 前端开发
Javascript中的闭包encloure
【10月更文挑战第1天】闭包是 JavaScript 中一种重要的概念,指函数能够访问其定义时的作用域内的变量,即使该函数在其词法作用域之外执行。闭包由函数及其词法环境组成。作用域链和词法作用域是闭包的核心原理。闭包常用于数据隐藏和封装,如模块模式;在异步操作中也广泛应用,如定时器和事件处理。然而,闭包也可能导致内存泄漏和变量共享问题,需谨慎使用。
|
30天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript闭包:原理与应用
【10月更文挑战第11天】深入理解JavaScript闭包:原理与应用
19 0
|
2月前
|
JSON JavaScript 前端开发
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级
JavaScript第五天(函数,this,严格模式,高阶函数,闭包,递归,正则,ES6)高级