JavaScript 作用域
JavaScript 作用域
Section titled “JavaScript 作用域”JavaScript 中的作用域(Scope)决定了变量(variables)、函数(functions)和对象(objects)在运行时(runtime)的可访问性(可见性)。理解作用域对于编写可预测且无 bug 的代码至关重要。
JavaScript 中的作用域类型
Section titled “JavaScript 中的作用域类型”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)
Section titled “函数作用域(var)”使用 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)会被移动到顶部。如果不理解这一点,有时可能导致令人困惑的行为。
块级作用域(let 和 const)
Section titled “块级作用域(let 和 const)”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();严格模式与未声明变量
Section titled “严格模式与未声明变量”在非严格模式(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 来声明变量。
变量生命周期
Section titled “变量生命周期”- 全局变量(Global variables): 生命周期与应用程序(网页或 Node.js 进程)一致。
- 局部变量(Local variables,函数作用域或块级作用域): 在其包含的函数被调用或块被进入时创建,并在函数/块执行结束时通常会被销毁(垃圾回收,garbage collected),除非它们是仍然可访问的闭包(closure)的一部分。
函数参数(parameters)在函数作用域内也表现得像局部变量(local variables)。
作用域链和变量遮蔽
Section titled “作用域链和变量遮蔽”当 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"作用域的最佳实践
Section titled “作用域的最佳实践”- 尽量减少全局变量(global variables)的使用。使用模块(modules)(ES6 模块或旧模式)来封装(encapsulate)代码。
- 优先使用
let和const,而不是var,以实现块级作用域(block scoping)并避免常见的var陷阱(pitfalls)。 - 在尽可能窄的作用域内声明变量。
- 使用
'use strict';来捕获常见的错误,例如意外的全局变量。