🤔

【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 引入了 letconst,让 {}(大括号)也能约束变量的范围。

  • if 语句、for 循环、while 循环的大括号都会产生块级作用域。
  • 注意var 定义的变量不受块级作用域约束,它会“逃逸”出去。

二、 核心精髓:词法作用域 (Lexical Scope)

这是 JavaScript 最重要的特性之一。

词法作用域(也叫静态作用域)是指:作用域在代码书写时(或者说编译器解析时)就确定了,而不是在执行时确定。

请看这个经典的面试题:

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar(); // 结果是 1 还是 2?

解析:

  1. foo 函数在定义时,它的“上级地盘”就是全局作用域。
  2. 无论 foo 在哪里被调用(即便是在 bar 内部),它查找变量时都会回到它定义时的位置。
  3. 它在自己地盘没找到 value,就去上级(全局)找,找到了 1
  4. 结论:结果是 1

三、 作用域链 (Scope Chain):变量的“寻亲记”

当 JavaScript 需要使用一个变量时,它会开启一场“寻亲之旅”:

  1. 当前作用域:先在当前房间找,找到了就直接用。
  2. 外层作用域:如果没找到,就去“亲生父母”的作用域找。
  3. 继续向上:直到找到全局作用域(顶层)。
  4. 终点:如果全局还没找到,就会报错(ReferenceError)。

比喻:这就像你在家里找遥控器,客厅没有就去卧室找,卧室没有就去阳台找,直到把整个房子翻遍。这种一层套一层的查找关系,就是作用域链


四、 变量提升 (Hoisting) 与作用域

在作用域内部,还有一个有趣的现象:变量提升

function test() {
  console.log(a); // 输出 undefined,而不是报错
  var a = 10;
}

真相:引擎在处理作用域时,会把所有的 var 声明和 function 声明“挪”到当前作用域的最顶端。

  • var 提升时只提升声明,不提升赋值(所以是 undefined)。
  • letconst 也有提升,但它们存在暂时性死区 (TDZ),在声明前访问会直接报错,这让代码更规范。

五、 为什么需要作用域?

  1. 隔离变量:不同作用域下同名变量不会冲突(比如不同函数里都可以有 i)。
  2. 安全性:外部无法随意修改函数内部的隐私数据。
  3. 内存管理:当作用域执行完且没有被引用(如闭包)时,内部变量可以被垃圾回收,节省内存。

结语

作用域是 JavaScript 的静态骨架。它在代码写下的那一刻,就画好了边界。

理解了作用域,你就理解了变量的“可见性”。但变量有了地盘,代码又是如何跑起来的呢?这就涉及到下一篇我们要讲的——执行上下文。那是一个关于“动态现场”的故事。