JavaScript 的词法作用域
JavaScript 是一种动态语言,其作用域机制在开发中扮演了重要角色。词法作用域(Lexical Scope)是 JavaScript 中作用域的一种基础规则,了解它可以更好地编写高效、可维护的代码。
什么是词法作用域?
词法作用域(Lexical Scope),也称为静态作用域(Static Scope),是指变量的作用域在代码编译时就确定了,而不是在运行时确定。换句话说,一个变量的作用域是由它在源代码中声明的位置决定的。
词法作用域的规则
- 查找变量时,JavaScript 引擎会按照代码的书写结构(词法结构)逐层向上查找。 它首先在当前作用域中查找变量,如果没有找到,就到外层作用域查找,直到找到该变量或到达全局作用域为止。这个查找的过程形成了一个作用域链(Scope Chain)。
- 内部函数可以访问其外部函数的作用域,但外部函数不能访问内部函数的作用域。 这就形成了“闭包”的基础。
示例
function outerFunction() {
const outerVariable = "I am from outer";
function innerFunction() {
console.log(outerVariable); // 可以访问 outerVariable
}
return innerFunction;
}
const myFunction = outerFunction();
myFunction(); // 输出:I am from outer
在这个例子中,innerFunction 可以访问 outerVariable,是因为它在定义时的词法作用域中包含了 outerVariable,即使 outerFunction 已经执行完毕,outerVariable 依然可以被访问。
为什么词法作用域重要?
- 封装性:词法作用域允许开发者封装变量和逻辑,从而避免全局污染。
- 闭包的实现基础:闭包依赖于词法作用域,可以让函数记住定义时的上下文。
- 代码可预测性:由于作用域在定义时就确定,代码行为更易预测。
词法作用域与动态作用域的区别
与词法作用域相对的是动态作用域(Dynamic Scope)。动态作用域是指变量的作用域在运行时确定,取决于函数在哪里被调用,而不是在哪里被定义。JavaScript 使用的是词法作用域,而不是动态作用域
动态作用域示例
(JavaScript 不支持动态作用域,但我们用伪代码说明)
function outerFunction() {
const outerVariable = "I am from outer";
innerFunction();
}
function innerFunction() {
console.log(outerVariable); // 假如是动态作用域,这会查找调用时的上下文
}
outerFunction(); // 如果是动态作用域,将输出:I am from outer
在 JavaScript 中,以上代码会抛出 ReferenceError,因为 innerFunction 的作用域在定义时已经确定。
如何利用词法作用域优化代码?
-
模块化开发: 利用词法作用域,将变量和逻辑封装在函数中。
function createCounter() { let count = 0; return function() { return ++count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 -
避免全局污染: 使用 IIFE(立即执行函数)创建局部作用域。
(function() { const localVariable = "I am local"; console.log(localVariable); })(); // console.log(localVariable); // ReferenceError -
使用
const和let代替var:const和let遵循块级作用域,而var是函数作用域,容易引发意外行为。{ let blockScoped = "I am block scoped"; console.log(blockScoped); // 有效 } // console.log(blockScoped); // ReferenceError
常见面试题
-
下面代码输出什么?为什么?
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); }答案:输出 3, 3, 3。因为
var是函数作用域,所有的setTimeout回调共享同一个i,循环结束时i的值为 3。 -
如何修改上面的代码使其输出 0, 1, 2? 答案:使用
let或 IIFE。for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); }或
for (var i = 0; i < 3; i++) { (function(i) { setTimeout(() => console.log(i), 1000); })(i); } -
解释以下代码中
console.log的输出const outer = () => { let count = 0; return () => { count++; console.log(count); }; }; const counter1 = outer(); const counter2 = outer(); counter1(); // ? counter1(); // ? counter2(); // ?答案:输出分别是 1, 2, 1。因为每次调用
outer会创建新的闭包,counter1和counter2拥有各自独立的count变量。 -
下面代码为什么会报错?如何修复?
{ console.log(blockScopedVar); // ReferenceError let blockScopedVar = "I am block scoped"; }答案:因为
let存在暂时性死区(TDZ),在变量声明之前访问会报错。将console.log放到变量声明之后即可修复。{ let blockScopedVar = "I am block scoped"; console.log(blockScopedVar); // I am block scoped }