JavaScript从入门到入土(4):理解闭包是什么的保姆级教程

一、闭包的引出

1.1 作用域链和调用栈

作用域链是 JavaScript 引擎在查找变量时使用的 动态链表结构 ,它由多个 变量对象(Variable Object) 组成,每个变量对象对应一个作用域。当访问一个变量时,引擎会从当前作用域开始,逐级向上查找,直到找到变量或到达全局作用域。

作用域链的查找规则

  • JavaScript先在当前作用域中查找变量,如果没有找到,就会向上一级作用域中查找,直到找到全局作用域,还没有找到就会报错。

  • 作用域链的上一级由outer指针决定,outer指针指向当前作用域嵌套(在作用域一章有介绍)的外层。

js 复制代码
function foo() {
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)// 输出:1  当前作用域没有a,就会去上一级作用域找
      console.log(b)// 输出:3  当前作用域有b,就会直接使用
    }
    console.log(b)// 输出:2  输出当前作用域的b
    console.log(c)// 输出:4  var声明的c发生变量提升
    console.log(d)// 报错:ReferenceError: d is not defined  let声明的d不会发生变量提升,不能访问内层的d
    
  }
  foo()

在 JavaScript 中,调用栈 是一种后进先出(LIFO)的数据结构,用于存储代码执行过程中创建的执行上下文。每个函数调用都会创建一个新的执行上下文,并被压入调用栈的顶部;当函数执行完毕后,其执行上下文会从栈顶弹出。它负责跟踪代码的执行流程、函数调用关系以及变量的作用域。

执行上下文是 JavaScript 执行一段代码时的环境,它定义了变量和函数的作用域,并包含四个核心组件:

  • 变量环境(Variable Environment) :存储变量和函数的初始定义(ES6 之前的主要存储位置)。
  • 词法环境(Lexical Environment) :ES6 引入的概念,主要存储 letconst 声明的变量。
  • 作用域链(Scope Chain) :由当前变量对象和外层作用域的变量对象组成。
  • this 指针:指向当前执行上下文的对象。(暂时不介绍)

执行上下文的类型

  • 全局执行上下文:代码开始执行时创建的第一个上下文,全局变量和函数存储在此。
  • 函数执行上下文:每次函数调用时创建,包含函数内部的变量和参数。
  • Eval 执行上下文eval() 函数执行时创建(极少使用)。

调用栈的工作流程

js 复制代码
function greet(name) {
  return `Hello, ${name}!`;
}

function sayHi() {
  const message = greet("Alice");
  console.log(message);
}

sayHi();
  1. 全局调用了sayHi()函数,创建sayHi()函数执行上下文并压入调用栈
  2. 执行函数sayHi(),发现调用了greet()函数,创建greet()函数执行上下文并压入调用栈
  3. 执行greet()函数,返回Hello, Alice!greet()函数执行完毕,执行上下文会从栈顶弹出。
  4. 继续执行sayHi()函数,message被赋值Hello, Alice!,执行console.log(message);输出:Hello, Alice!
  5. sayHi()函数执行完毕,执行上下文会从栈顶弹出。程序也执行完毕。

1.2 为什么要有闭包

接下来让结合我们上面学习的作用域链和调用链相关知识分析下面代码的执行过程,这里也需要用到之前预编译的相关知识,不了解或忘记的可以复习JavaScript从入门到入土(3):预编译

js 复制代码
function outer() {
  let count = 0;
  function inner() {
      count++; 
      console.log(count);
  }
  return inner; 
}

const counter = outer();
counter();  
  1. 编译全局,调用栈结构图如下图所示
  1. 执行全局,调用outer()函数,创建outer()函数执行上下文并压入调用栈
  2. 编译函数,调用栈结构图如下图所示
  1. 执行函数,count赋值0,返回函数inner()给全局变量counterouter()函数执行完毕,执行上下文从栈顶弹出。
  1. 继续执行全局

继续分析我们知道调用counter()函数即调用inner()函数,于是创建inner函数执行上下文。调用栈如上图所示。

但继续执行inner函数我们会发现一个问题,outer()已经销毁了,存在于outer函数执行上下文的变量count还能被访问到吗?

答案是能,因为有闭包的存在,这也是为什么要有闭包的原因。

根据作用链的查找规则,内部函数一定有权利访问外部函数的变量。而根据调用栈的调用规则,一个函数执行完后函数执行上下文一定会被销毁。那么当函数A内部声明了一个函数B,而函数B拿到函数A外面执行(例如上面程序)的情况时。为了保证上面两个规则正常执行,函数A在执行完毕后会将B需要访问的变量保存到一个集合中,这个集合就是闭包。

引入了闭包这个帮手后,我们可以补充相关内容得到更准确的执行过程。

  1. 遇到 inner 函数定义时:
  • inner 函数被创建,同时捕获 outer 的词法环境(闭包形成)。
  • inner 的作用域链为:[inner 词法环境] → [outer 词法环境] → [全局词法环境]
  1. 返回 inner 函数:
  • outer 执行完毕,其执行上下文从调用栈弹出。
  • inner 函数的闭包 保留了对 outer 词法环境的引用,因此 count 变量不会被销毁。

由此我们可以得到当前的inner函数执行上下文实际如下图所示。

  1. 执行inner()

接下来的过程就很简单了,对count++后输出,即输出1。inner()执行完毕,弹出,全局执行完毕,弹出。

二、闭包的关键特性

1. 变量捕获与内存管理

闭包会捕获外层函数的变量引用,导致这些变量不会被垃圾回收。因此,滥用闭包可能导致内存泄漏。

示例

js 复制代码
function createClosure() {
  const largeArray = new Array(1000).fill('x'); // 占用大量内存
  return function() {
      console.log(largeArray.length);
  };
}

const closure = createClosure(); // largeArray 不会被回收,因为被闭包引用

2. 循环中的闭包陷阱

循环中使用闭包时,闭包捕获的是变量的引用,而非循环时的值。

错误示例

js 复制代码
for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i); // 输出 3, 3, 3(闭包捕获的是同一个 i 变量)
    }, 100);
}

正确解法

  • 使用 let(块级作用域):
js 复制代码
for (let i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i); // 输出 0, 1, 2(let 为每次循环创建独立的 i)
    }, 100);
}
  • 使用立即执行函数(IIFE):
js 复制代码
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => {
            console.log(j); // 输出 0, 1, 2(通过 IIFE 创建独立的 j)
        }, 100);
    })(i);
}

三、闭包的性能考量

  1. 内存占用:闭包会保留变量引用,可能导致内存占用增加。

  2. 垃圾回收 :若闭包不再需要,应手动解除引用(如将闭包设为 null)。

  3. 过度使用:避免在循环或频繁调用的函数中创建不必要的闭包。


理解闭包对掌握JavaScript非常重要,如果觉得这篇文章对你有帮助的话就点个赞吧!!

相关推荐
某公司摸鱼前端2 小时前
uniapp socket 封装 (可拿去直接用)
前端·javascript·websocket·uni-app
要加油哦~2 小时前
vue | 插件 | 移动文件的插件 —— move-file-cli 插件 的安装与使用
前端·javascript·vue.js
wen's3 小时前
React Native 0.79.4 中 [RCTView setColor:] 崩溃问题完整解决方案
javascript·react native·react.js
Alfred king3 小时前
面试150 生命游戏
leetcode·游戏·面试·数组
vvilkim4 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron
vvilkim4 小时前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron
aha-凯心4 小时前
vben 之 axios 封装
前端·javascript·学习
漫谈网络4 小时前
WebSocket 在前后端的完整使用流程
javascript·python·websocket
一只叫煤球的猫5 小时前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试
失落的多巴胺6 小时前
使用deepseek制作“喝什么奶茶”随机抽签小网页
javascript·css·css3·html5