闭包、块级作用域、作用域链详解

86 阅读5分钟

一、块级作用域详解

首先思考一下下面这段代码输出什么?

let data=[]
for(var i=0;i<3;i++){
    data[i]=function(){
        console.log(i)
    }
}
data[0]()
data[1]()
data[2]()

image.png

答案都是3

image.png

for循环里的ivar声明的,都知道var没有块级作用域,所以这个i属于函数作用域(或者是全局作用域),这意味着整个循环中只有一个 i 变量,每次循环只是修改这个变量的值(从 0 增至 3),而非创建新变量。 并且这段代码形成了一个闭包,闭包捕获的是变量的引用而不是值,console.log(i)这里的i捕获了全局作用域中i的引用。

在 JavaScript 中,闭包(Closure)  是指一个函数能够访问并操作其声明时所在的外部作用域中的变量,即使该外部函数已经执行完毕并退出。简单来说,闭包让函数 “记住” 了它诞生时的环境。
闭包的本质是作用域链的保留, JavaScript 中,函数在创建时会关联一个作用域链(Scope Chain),包含当前函数作用域和所有外层作用域的变量。
即使外层函数的执行上下文已销毁,其作用域链中的变量仍会被闭包引用,因此不会被垃圾回收机制清除。

详细拆解:

data[i] 中的 i 会随着循环自增而变化,但最终函数执行时输出 3 是因为 data[i] 存储的是函数本身,而函数内部引用的 i 是循环结束后的最终值

循环过程中,i 的值依次为 012,因此 data[i] 实际上是:

  • 当 i=0 时:data[0] = function() { console.log(i) }

  • 当 i=1 时:data[1] = function() { console.log(i) }

  • 当 i=2 时:data[2] = function() { console.log(i) }

可以看到,data 数组的索引 i 确实在随着循环自增(0→1→2),所以 data 数组最终会正确存储 3 个函数(分别对应索引 012)。

虽然 data[i] 的索引 i 在变化,但函数内部的  console.log(i) 引用的 i 是同一个全局 / 函数作用域的变量(因为 var i 没有块级作用域)。

  • 循环结束后,i 的值固定为 3(如前所述)。
  • 当后续调用 data[0]() 时,函数会去查找这个全局的 i,此时拿到的就是 3

简单说:data[i] 的索引 i 记录的是 “存储函数时的位置”,但函数内部的 i 记录的是 “未来执行时的变量值”。

** 类比理解**

可以把 i 想象成一个公共的计数器:

  • 循环时,你按顺序把 3 个函数放进了 data[0]data[1]data[2](此时计数器的值是 0、1、2)。
  • 但每个函数里写的是 “打印当前计数器的值”,而不是 “打印放进去时的计数器值”。
  • 循环结束后,计数器被加到了 3,所以无论调用哪个函数,都会打印 3

如果想让函数记住 “存储时的 i 值”,可以用 let 声明 i(块级作用域,每次循环创建独立的 i),或者用立即执行函数捕获当前值:

// 用 捕获当前 i 值 
var data = []; 
for (var i = 0; i < 3; i++) {
    (function(num) { // num 是当前 i 的副本 
        data[i] = function() {
        console.log(num); // 打印捕获的 num 
    }; 
  })(i); // 传入当前 i 
} 
    data[0](); // 0 
    data[1](); // 1 
    data[2](); // 2

二、作用域链详解

还是先看一下这段代码的输出:

var k=10 
const a=function(){
    console.log(k) 
} 
(function(b){
    var k=20
    b() 
})(a)

image.png

在 JavaScript 中,作用域链(Scope Chain)  是指由多个嵌套的作用域组成的链式结构,它决定了变量和函数的访问规则:当代码需要访问一个变量时,JavaScript 引擎会沿着这条链从当前作用域开始逐层向上查找,直到找到目标变量或抵达全局作用域为止。
核心特点:
由嵌套作用域构成
查找规则:就近原则 访问变量时,引擎先在当前作用域查找;若未找到,则沿作用域链向上层作用域查找,直到找到变量或抵达全局作用域(若全局也没有,则返回undefined)。若不同作用域存在同名变量,内层作用域的变量会覆盖外层
定义时确定,而非调用时:作用域链在函数定义时就已确定(基于函数声明的位置),而非调用时。这也是闭包能保留外层变量引用的核心原因

执行过程分析:

  1. 全局作用域声明 var k = 10,此时全局变量 k 的值为 10。

  2. 声明函数 a,其内部逻辑为 console.log(k),函数 a 在定义时的作用域链中,k 指向全局变量 k(10)。

  3. 立即执行函数(IIFE)接收参数 b(实际传入函数 a),并在内部声明局部变量 var k = 20(该 k 仅在 IIFE 内部有效)。

  4. 调用 b() 即执行函数 aa 内部查找 k 时,沿其定义时的作用域链找到全局变量 k = 10,因此输出 10

核心原因:函数的作用域链在定义时确定,而非调用时。函数 a 定义在全局作用域,因此始终访问全局的 k,与 IIFE 内部的局部 k 无关。

希望看着这一篇文章,能让你对块级作用域,闭包,作用域链能有更深刻的理解!