JavaScript: JavaScript 作用域
最后更新:2026-10-13
作用域决定了变量"在哪儿能被访问到"。你可以把作用域想象成身份证——出了省,你的市身份证就不好使了;出了国,省身份证也不认了。变量也是一样,出了自己的作用域就找不到了。
1. 作用域的类型与规则
在最外层用 var、let 或 const 声明的变量拥有全局作用域,任何地方都能访问。
HTML
<script>
var globalVar = "我是全局变量";
function test() {
console.log(globalVar); // 能访问
}
test();
</script>
全局变量虽然方便,但容易"污染命名空间"——项目一大,全局变量名冲突是家常便饭。尽量少用全局变量。
(2) 函数作用域
var 声明的变量遵循函数作用域:只在当前函数内有效,出了函数就消失。
HTML
<script>
function greet() {
var message = "你好";
console.log(message); // 正常
}
greet();
try {
console.log(message); // ReferenceError!
} catch(e) {
console.log("访问失败:" + e.message);
}
</script>
var 的函数作用域有个经典坑:在 for 循环里用 var,循环结束后变量还"漏"在外面。
(3) 块级作用域
let 和 const 声明的变量遵循块级作用域:只在当前 {} 内有效。
HTML
<script>
if (true) {
let x = 10;
const y = 20;
var z = 30;
console.log("块内:x=" + x + ", y=" + y + ", z=" + z);
}
console.log("块外:z=" + z); // var 不受块级限制
try {
console.log(x); // x 在这里不可访问
} catch(e) {
console.log("x 不可访问:" + e.message);
}
</script>
这就是为什么循环计数器要用 let 而不是 var。
(4) 作用域链
内层作用域可以访问外层作用域的变量,就像你在学校里可以用家里的东西(如果带去了的话)。查找变量时,从当前作用域向外一层层找,直到全局——这条链就是作用域链。
HTML
<script>
const city = "北京";
function outer() {
const district = "海淀";
function inner() {
const street = "中关村";
console.log(street, district, city); // 都能访问
}
inner();
}
outer();
</script>
外层不能访问内层的变量——就像你不能进学校拿学生口袋里的东西。
(5) 变量提升(Hoisting)
var 声明会被"提升"到作用域顶部,但赋值不会:
HTML
<script>
console.log(a); // undefined(不是报错!)
var a = 5;
// 引擎实际执行顺序:var a; → console.log(a); → a = 5;
</script>
let 和 const 也有提升,但在声明前访问会报错——这段区域叫暂时性死区(TDZ):
HTML
<script>
try {
console.log(b); // ReferenceError!
} catch(e) {
console.log("暂时性死区:" + e.message);
}
let b = 5;
</script>
结论:永远用 let 和 const,避免 var 的提升陷阱。
(6) 闭包的直观理解
闭包 = 函数 + 它诞生时的变量环境。就像一个人离开了家乡,但心里记得家乡的样子——即使家乡后来变了,他记忆中的家乡不变。
HTML
<script>
function createCounter() {
let count = 0; // 这个变量被"关"在闭包里
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量没有被销毁,因为返回的函数还"记着"它
</script>
createCounter 已经执行完了,按理说 count 该销毁了——但返回的函数紧紧抓着 count 不放,所以它活了下来。这就是闭包。
(7) 闭包的实用场景
- 计数器:如上例,不需要全局变量就能记录状态
- 私有变量:外部无法直接访问闭包内的变量,只能通过返回的函数操作
- 回调函数:事件监听、定时器中保持对变量的引用
▶ 示例:var vs let 在循环中的区别
▶ 试一试▶ 示例:作用域链演示
▶ 试一试▶ 示例:闭包计数器与私有变量
▶ 试一试▶ 示例:变量提升对比
▶ 试一试❓ 常见问题
Q 为什么
for 循环用 var 会导致所有回调都输出同一个值?A 因为
var 只有函数作用域,循环结束后只有一个 i,所有回调共享同一个 i(此时已经等于循环终值)。用 let 则每次迭代都有独立的 i 副本。Q 闭包会造成内存泄漏吗?
A 闭包本身不是内存泄漏——它是有意保持对变量的引用。真正的泄漏是:你不再需要某个闭包了,但它还被别的引用链挂着,导致变量无法被回收。解决办法:将引用置为
null。Q
let 和 const 该选哪个?A 默认用
const,只有需要重新赋值时才用 let。这个习惯能防止你意外修改不该改的变量。📖 小节
- 全局作用域在整个程序中可见,函数作用域只在函数内可见,块级作用域只在
{}内可见 - 作用域链:变量查找从内到外,内层可以访问外层变量,外层不能访问内层
var有函数作用域和变量提升,let/const有块级作用域和暂时性死区- 闭包是函数"记住"其创建时作用域的能力,用于数据私有化和工厂函数
- 尽量避免全局变量,用函数或模块封装代码,减少命名冲突
📝 作业
- 写一个函数
makeGreeter(greeting),返回一个函数,调用时返回greeting + "," + name。用两个不同的greeting创建两个问候函数,验证它们互不干扰。 - 写一个
createWallet(initialBalance)函数,返回deposit、withdraw、getBalance三个方法,balance从外部无法直接访问(私有变量),只能通过方法操作。 - 解释下面代码的输出结果,并说明为什么:
HTML
<script>
for (var i = 0; i < 3; i++) {
setTimeout(function() { console.log(i); }, 100);
}
</script>
把它改成输出 0、1、2。