为什么有的变量在函数里能用,在外面却报错?为什么循环里的i总是最后一个值?今天我们就来聊聊JavaScript的作用域和作用域链,顺便揭开闭包的神秘面纱。保证你看完之后,再也不用背面试题了。
前言
想象一下这样的场景:你在自己房间里找手机,找不到就去客厅找,再找不到就去邻居家借手机打电话。如果所有地方都找不到,那就只能放弃------手机丢了。
JavaScript在查找变量时,也是这么个流程。这个"找东西"的规则,就是作用域链 。而变量能在哪些地方被找到,由它的作用域决定。
今天我们就来把这件事彻底捋清楚。
一、作用域:变量的"活动范围"
作用域就是变量能够被访问到的范围。JS中有三种主要作用域:
1. 全局作用域:公共场所
在函数外面定义的变量,或者没加任何关键字直接写的变量(严格模式会报错),都属于全局作用域。
js
var globalVar = '我是全局的';
let alsoGlobal = '我也是全局的';
function sayHello() {
console.log(globalVar); // 能访问
}
全局变量就像公共场所的设施,谁都能用,但正因为谁都能改,所以容易出问题。而且全局变量会一直存在,直到页面关闭。
2. 函数作用域:自己家
在函数内部用var声明的变量,只能在这个函数内部访问。外面进不去,里面可以出去(找外面的变量)。
js
function myHouse() {
var secret = '我藏起来的零食';
console.log(secret); // 能访问
}
console.log(secret); // 报错:secret is not defined
函数作用域像自己家,外人不能随便进,但你可以从家里出去(访问全局)。
3. 块级作用域:卧室里的保险柜
ES6新增的let和const带来了块级作用域。块就是大括号{}包起来的地方,比如if、for、while里面。
js
if (true) {
let blockVar = '我只能在块里用';
var functionVar = '我可以在整个函数用'; // var没有块级作用域
}
console.log(blockVar); // 报错
console.log(functionVar); // 能访问,因为var只有函数作用域
块级作用域就像卧室里的保险柜,只有在这个房间里才能打开。var则像家里的公共区域,虽然写在卧室里,但实际还是公共的。
二、作用域链:找变量的路径
当你在一个作用域里使用变量时,JS引擎会按照这个顺序找:
- 当前作用域:先看自己家里有没有。
- 外层作用域:没有就去上一层找。
- 继续往外:一层一层往上,直到全局作用域。
- 全局也没有 :那就报错
not defined。
这种嵌套的作用域形成的链条,就是作用域链。
来看个例子:
js
var global = '全球通';
function outer() {
var outerVar = '外层的';
function inner() {
var innerVar = '内层的';
console.log(innerVar); // 找到自己家的
console.log(outerVar); // 自己家没有,去外层找
console.log(global); // 自己家没有,外层没有,再去全局
}
inner();
}
outer();
这个过程就像你在家找东西:先翻自己口袋,没有就去客厅找,还没有就去小区便利店,再没有就只能放弃了。
三、闭包:虽然离开了,但我还记得
闭包是JS里一个常考常新、常学常忘的概念。简单来说:闭包就是函数记住了它定义时的作用域,即使这个函数在其他地方执行,也能访问那个作用域里的变量。
举个例子:
js
function createCounter() {
let count = 0; // count 被闭包记住了
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
这里createCounter执行后返回了一个函数,按说count应该被销毁了,但返回的函数依然能访问count------这就是闭包的力量。
闭包的生活比喻
想象你从小长大的家,后来搬走了,但你还记得家里的WiFi密码。每次你路过楼下,还能连上那个WiFi。这个"记住密码"的能力,就是闭包。
闭包的用途:
- 数据私有化(比如上面的计数器,外部无法直接修改count)
- 函数工厂(生成特定功能的函数)
- 回调函数中保持状态(比如事件监听)
闭包的坑
闭包虽然好用,但也要注意内存问题。因为被记住的变量不会释放,如果闭包一直存在,这些变量就会一直占用内存。比如上面例子,只要counter这个函数还在,count就不会被垃圾回收。
四、经典面试题:循环中的var
这是JS初学者最容易踩的坑之一:
js
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
你期望输出0,1,2,3,4,但实际输出5,5,5,5,5。为什么?
因为var没有块级作用域,循环里的i其实是全局(或函数级)的同一个变量。循环结束后i变成了5,然后setTimeout的回调执行时,访问的都是同一个i,所以全是5。
解决方式:
- 用let:let有块级作用域,每次循环都会创建一个新的变量。
js
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0,1,2,3,4
}, 100);
}
- 用闭包(老办法):
js
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
用立即执行函数创建新的作用域,把每次的i传进去保存下来。
五、词法作用域:写在哪就在哪找
JS采用的是词法作用域 (也叫静态作用域),也就是说变量的查找范围在代码编写时就决定了,而不是在运行时。
js
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo(); // 输出什么?
}
bar(); // 输出1
这里foo定义在全局,所以它访问的value是全局的1,而不是bar里的2。因为作用域由函数定义的位置决定,而不是调用位置。
这个特性是闭包能工作的基础。
六、执行上下文:运行时的小剧场
作用域是静态的规则,而执行上下文是运行时动态的环境。每当函数执行,都会创建自己的执行上下文,里面包含了变量、参数、以及对外部作用域的引用。
执行上下文有点像每次进家门时拿的钥匙串,上面有自己家的钥匙,还有父母家的钥匙(通过作用域链)。
七、总结:今天你学到了什么?
- 作用域就是变量的可见范围:全局(公共场所)、函数(自己家)、块级(卧室保险柜)。
- 作用域链就是找变量的路径:当前 → 外层 → 全局,找不到就报错。
- 闭包是函数记住了它出生时的环境,即使离开了也能访问那些变量。用途广泛,但要注意内存。
- 词法作用域意味着变量的查找在写代码时决定,和运行位置无关。
- 循环中用
var容易踩坑,用let或闭包解决。
现在你再看到作用域相关的问题,应该能像老司机一样游刃有余了。明天我们将继续深入,聊聊JavaScript里最让人迷惑的概念之一:闭包的应用场景和内存管理,看看闭包在实际项目中到底怎么用,怎么避免内存泄漏。
如果你觉得今天的文章对你有帮助,点个赞让更多人看到,也欢迎在评论区聊聊你遇到过的作用域坑。我们明天见!