Skip to content

JavaScript 作用域

JavaScript 中的作用域(Scope)决定了变量(variables)、函数(functions)和对象(objects)在运行时(runtime)的可访问性(可见性)。理解作用域对于编写可预测且无 bug 的代码至关重要。

JavaScript 主要有三种作用域类型:

  • 全局作用域(Global Scope): 在任何函数或块外部声明的变量具有全局作用域。它们可以从 JavaScript 代码中的任何位置访问。
  • 函数作用域(Function Scope,var 的局部作用域): 使用 var 在函数内部声明的变量作用域为整个函数。它们不能在函数外部访问。
  • 块级作用域(Block Scope,let 和 const 的局部作用域): 使用 let 或 const 在块(例如,像 if 语句、for 循环或仅是一个独立块中用花括号 {...} 括起来的代码)内部声明的变量作用域为该块。它们不能在该块外部访问。

在任何函数或块外部声明的变量成为全局变量(global variable)。

const globalMessage = "Hello from global scope!"; // 全局变量
function showMessage() {
console.log(globalMessage); // 在函数内部可访问
}
showMessage();
console.log(globalMessage); // 在函数外部可访问

在浏览器环境中,全局变量通常会成为 window 对象(object)的属性(properties)。

var globalVarInBrowser = "I am on window"; // 使用 var 声明的变量会成为 window 对象的属性
let globalLetInBrowser = "I am not directly on window"; // 不是 window 对象的直接属性
const globalConstInBrowser = "Me neither"; // 不是 window 对象的直接属性
console.log(window.globalVarInBrowser); // "I am on window"
// console.log(window.globalLetInBrowser); // undefined

全局变量注意事项: 过度使用全局变量可能导致命名冲突(naming conflicts),并使代码更难维护(maintain)和调试(debug)。通常最佳实践(best practice)是尽量减少它们的使用。

使用 var 在函数内部声明的变量可以在该函数内部的任何地方访问,甚至在其声明之前(由于变量提升,hoisting),但不能在其外部访问。

function myFunctionScope() {
var carName = "Volvo"; // 函数作用域
console.log("Inside function: " + carName); // 函数内部可访问:Volvo
if (true) {
var color = "Red"; // 仍然是函数作用域,不是块级作用域
}
console.log("Color inside function: " + color); // 可访问
}
myFunctionScope();
// console.log(carName); // 错误:carName 在函数外部未定义
// console.log(color); // 错误:color 在函数外部未定义

使用 var 创建的变量会被提升(hoisted)到其函数作用域的顶部,这意味着它们的声明(declaration)(但不是初始化 initialization)会被移动到顶部。如果不理解这一点,有时可能导致令人困惑的行为。

ES6 引入了 let 和 const,它们提供了块级作用域(block scope)。这意味着使用 let 或 const 声明的变量只能在其定义所在的块({...})内部访问。

function myBlockScope() {
let vehicle = "Car"; // 函数作用域(块即函数本身)
const year = 2023;
if (true) {
let model = "Tesla"; // 作用域限定在该 if 块
const isElectric = true; // 作用域限定在该 if 块
console.log("Inside if-block: " + model); // 可访问
console.log("Vehicle in if: " + vehicle); // 可访问(来自外部作用域)
}
// console.log(model); // 错误:model 在此处未定义
// console.log(isElectric); // 错误:isElectric 在此处未定义
console.log("Outside if-block: " + vehicle);
}
myBlockScope();
{
let isolatedVar = "I am in a standalone block"; // 我在一个独立的块中
console.log(isolatedVar);
}
// console.log(isolatedVar); // 错误:isolatedVar 在此处未定义

let 和 const 也会被提升(hoisted),但与 var 不同,它们不会被初始化(initialized)。在声明之前访问它们会导致 ReferenceError。从块开始到声明之间的这个时期通常被称为“暂时性死区”(Temporal Dead Zone,简称 TDZ)。

块级作用域有助于防止与变量重复声明(redeclaration)和意外修改(unintended modifications)相关的 bug,使代码更清晰、更可预测。

词法作用域(Lexical Scope,也称静态作用域 Static Scope)

Section titled “词法作用域(Lexical Scope,也称静态作用域 Static Scope)”

JavaScript 使用词法作用域(lexical scoping)。这意味着变量的作用域由其在编写(lexed/parsed)代码时在源代码中的位置决定,而不是由函数调用的位置决定。

内部函数(inner function)可以访问其外部函数(outer functions)的作用域。这就是闭包(closures)之所以可能的原因。

const outerValue = "Outer";
function outer() {
const middleValue = "Middle";
function inner() {
const innerValue = "Inner";
console.log(outerValue); // 访问全局作用域
console.log(middleValue); // 访问外部函数的作用域
console.log(innerValue); // 访问其自身作用域
}
inner();
}
outer();

在非严格模式(non-strict mode)下,为未声明的变量(undeclared variable)赋值会自动创建一个全局变量(global variable)。这是常见的 bug 源头。

function createGlobalImplicitly() {
// 在非严格模式下,这会创建一个名为 'implicitGlobal' 的全局变量
// 在严格模式下,这会抛出 ReferenceError
implicitGlobal = "I am an accidental global!";
}
// createGlobalImplicitly();
// console.log(implicitGlobal); // 如果不是在严格模式下,可在全局访问

**严格模式(Strict Mode,'use strict';)**有助于阻止这种情况。当启用严格模式时(通过在脚本或函数开头添加 'use strict';),给未声明的变量赋值会抛出 ReferenceError。

function strictFunction() {
'use strict';
// undeclaredVar = "This will throw an error"; // 这会抛出一个错误
let declaredVar = "This is fine"; // 这是正常的
console.log(declaredVar);
}
// strictFunction();

强烈建议始终使用严格模式,并始终使用 let、const 或(对于新代码来说不太推荐的)var 来声明变量。

  • 全局变量(Global variables): 生命周期与应用程序(网页或 Node.js 进程)一致。
  • 局部变量(Local variables,函数作用域或块级作用域): 在其包含的函数被调用或块被进入时创建,并在函数/块执行结束时通常会被销毁(垃圾回收,garbage collected),除非它们是仍然可访问的闭包(closure)的一部分。

函数参数(parameters)在函数作用域内也表现得像局部变量(local variables)。

当 JavaScript 查找变量时,它首先从当前作用域(current scope)开始,如果找不到,则沿着作用域链(scope chain)向上移动(到父级作用域 parent scopes),直到找到该变量或到达全局作用域(global scope)。如果在全局作用域中仍找不到,则会发生 ReferenceError。

如果内部作用域(inner scope)中的变量与外部作用域(outer scope)中的变量同名,则内部变量会“遮蔽”(shadows,即隐藏)外部变量。内部作用域将使用其自身的变量。

let x = "global x";
function testShadowing() {
let x = "local x in function"; // 遮蔽全局变量 x
console.log(x); // "local x in function"
if (true) {
let x = "local x in block"; // 遮蔽函数作用域中的 x
console.log(x); // "local x in block"
}
console.log(x); // "local x in function"
}
testShadowing();
console.log(x); // "global x"
  • 尽量减少全局变量(global variables)的使用。使用模块(modules)(ES6 模块或旧模式)来封装(encapsulate)代码。
  • 优先使用 let 和 const,而不是 var,以实现块级作用域(block scoping)并避免常见的 var 陷阱(pitfalls)。
  • 在尽可能窄的作用域内声明变量。
  • 使用 'use strict'; 来捕获常见的错误,例如意外的全局变量。