【JS 深度系列】第一篇:作用域——变量的“地盘”谁说了算?
在 JavaScript 的世界里,你是否遇到过 ReferenceError: x is not defined?或者明明在函数外定义了变量,函数内部却拿不到?
这些问题的核心都指向同一个概念:作用域 (Scope)。
如果把 JavaScript 引擎比作一个行政长官,那么作用域就是他手里的一张**“行政区划图”**。它规定了哪些变量在哪些地方可以被访问,哪些地方是“禁区”。
一、 什么是作用域?
简单来说,作用域就是一套规则,用于确定在何处以及如何查找变量。
它决定了代码执行时对变量的访问权限。在 JS 中,作用域主要分为三类:
1. 全局作用域 (Global Scope)
这是最外层的作用域。在代码中任何地方都能访问到的变量,就处在全局作用域中。
- 生命周期:伴随整个程序运行始终。
- 风险:过多定义全局变量会造成“全局污染”,容易引发命名冲突。
2. 函数作用域 (Function Scope)
在函数内部定义的变量,只能在函数内部访问。
function sayHello() {
var message = "Hello JS";
console.log(message); // 内部可见
}
sayHello();
console.log(message); // 报错:ReferenceError: message is not defined
3. 块级作用域 (Block Scope) —— ES6 新增
在 ES6 之前,JS 只有全局和函数作用域。ES6 引入了 let 和 const,让 {}(大括号)也能约束变量的范围。
if语句、for循环、while循环的大括号都会产生块级作用域。- 注意:
var定义的变量不受块级作用域约束,它会“逃逸”出去。
二、 核心精髓:词法作用域 (Lexical Scope)
这是 JavaScript 最重要的特性之一。
词法作用域(也叫静态作用域)是指:作用域在代码书写时(或者说编译器解析时)就确定了,而不是在执行时确定。
请看这个经典的面试题:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 结果是 1 还是 2?
解析:
foo函数在定义时,它的“上级地盘”就是全局作用域。- 无论
foo在哪里被调用(即便是在bar内部),它查找变量时都会回到它定义时的位置。 - 它在自己地盘没找到
value,就去上级(全局)找,找到了1。 - 结论:结果是
1。
三、 作用域链 (Scope Chain):变量的“寻亲记”
当 JavaScript 需要使用一个变量时,它会开启一场“寻亲之旅”:
- 当前作用域:先在当前房间找,找到了就直接用。
- 外层作用域:如果没找到,就去“亲生父母”的作用域找。
- 继续向上:直到找到全局作用域(顶层)。
- 终点:如果全局还没找到,就会报错(
ReferenceError)。
比喻:这就像你在家里找遥控器,客厅没有就去卧室找,卧室没有就去阳台找,直到把整个房子翻遍。这种一层套一层的查找关系,就是作用域链。
四、 变量提升 (Hoisting) 与作用域
在作用域内部,还有一个有趣的现象:变量提升。
function test() {
console.log(a); // 输出 undefined,而不是报错
var a = 10;
}
真相:引擎在处理作用域时,会把所有的 var 声明和 function 声明“挪”到当前作用域的最顶端。
var提升时只提升声明,不提升赋值(所以是undefined)。let和const也有提升,但它们存在暂时性死区 (TDZ),在声明前访问会直接报错,这让代码更规范。
五、 为什么需要作用域?
- 隔离变量:不同作用域下同名变量不会冲突(比如不同函数里都可以有
i)。 - 安全性:外部无法随意修改函数内部的隐私数据。
- 内存管理:当作用域执行完且没有被引用(如闭包)时,内部变量可以被垃圾回收,节省内存。
结语
作用域是 JavaScript 的静态骨架。它在代码写下的那一刻,就画好了边界。
理解了作用域,你就理解了变量的“可见性”。但变量有了地盘,代码又是如何跑起来的呢?这就涉及到下一篇我们要讲的——执行上下文。那是一个关于“动态现场”的故事。