一、块级作用域详解
首先思考一下下面这段代码输出什么?
let data=[]
for(var i=0;i<3;i++){
data[i]=function(){
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
答案都是3
for循环里的i
是 var
声明的,都知道var
没有块级作用域,所以这个i
属于函数作用域(或者是全局作用域),这意味着整个循环中只有一个 i
变量,每次循环只是修改这个变量的值(从 0
增至 3
),而非创建新变量。
并且这段代码形成了一个闭包,闭包捕获的是变量的引用而不是值,console.log(i)
这里的i
捕获了全局作用域中i
的引用。
在 JavaScript 中,闭包(Closure) 是指一个函数能够访问并操作其声明时所在的外部作用域中的变量,即使该外部函数已经执行完毕并退出。简单来说,闭包让函数 “记住” 了它诞生时的环境。
闭包的本质是作用域链的保留, JavaScript 中,函数在创建时会关联一个作用域链(Scope Chain),包含当前函数作用域和所有外层作用域的变量。
即使外层函数的执行上下文已销毁,其作用域链中的变量仍会被闭包引用,因此不会被垃圾回收机制清除。
详细拆解:
data[i]
中的 i
会随着循环自增而变化,但最终函数执行时输出 3
是因为 data[i]
存储的是函数本身,而函数内部引用的 i
是循环结束后的最终值。
循环过程中,i
的值依次为 0
、1
、2
,因此 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 个函数(分别对应索引 0
、1
、2
)。
虽然 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)
在 JavaScript 中,作用域链(Scope Chain) 是指由多个嵌套的作用域组成的链式结构,它决定了变量和函数的访问规则:当代码需要访问一个变量时,JavaScript 引擎会沿着这条链从当前作用域开始逐层向上查找,直到找到目标变量或抵达全局作用域为止。
核心特点:
由嵌套作用域构成
查找规则:就近原则 访问变量时,引擎先在当前作用域查找;若未找到,则沿作用域链向上层作用域查找,直到找到变量或抵达全局作用域(若全局也没有,则返回undefined)。若不同作用域存在同名变量,内层作用域的变量会覆盖外层
定义时确定,而非调用时:作用域链在函数定义时就已确定(基于函数声明的位置),而非调用时。这也是闭包能保留外层变量引用的核心原因
执行过程分析:
-
全局作用域声明
var k = 10
,此时全局变量k
的值为 10。 -
声明函数
a
,其内部逻辑为console.log(k)
,函数a
在定义时的作用域链中,k
指向全局变量k
(10)。 -
立即执行函数(IIFE)接收参数
b
(实际传入函数a
),并在内部声明局部变量var k = 20
(该k
仅在 IIFE 内部有效)。 -
调用
b()
即执行函数a
,a
内部查找k
时,沿其定义时的作用域链找到全局变量k = 10
,因此输出 10。
核心原因:函数的作用域链在定义时确定,而非调用时。函数 a
定义在全局作用域,因此始终访问全局的 k
,与 IIFE 内部的局部 k
无关。
希望看着这一篇文章,能让你对块级作用域,闭包,作用域链能有更深刻的理解!