JavaScript 词法作用域
什么是词法作用域?
词法作用域(Lexical Scope)是JavaScript中变量可见性和访问权限的一种规则体系。"词法"一词来源于这种作用域是在代码编写阶段(即词法分析时)就已确定,而不是在运行时确定的特点。
简单来说,词法作用域决定了代码中的变量在哪些区域可以被访问,在哪些区域不能被访问。它遵循一个基本原则:内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。
词法作用域也被称为"静态作用域",因为它在代码编写时(静态分析阶段)就已经确定,与代码如何执行无关。
词法作用域的基本规则
JavaScript的词法作用域遵循以下基本规则:
- 查找变量时,先在当前作用域查找
- 如果当前作用域没有找到,则向外层作用域查找
- 逐层向外查找,直到全局作用域
- 如果全局作用域也没有,则返回
undefined
或抛出ReferenceError
让我们用代码来看一个简单的例子:
// 全局作用域
const global = "I am global";
function outer() {
// outer函数作用域
const outer = "I am outer";
function inner() {
// inner函数作用域
const inner = "I am inner";
console.log(inner); // 访问自己作用域的变量
console.log(outer); // 访问外层作用域的变量
console.log(global); // 访问全局作用域的变量
}
inner();
// console.log(inner); // 错误!不能访问内层作用域的变量
}
outer();
输出结果:
I am inner
I am outer
I am global
作用域嵌套与查找规则
作用域是可以嵌套的,这创建了一个作用域链。当我们在代码中引用一个变量时,JavaScript引擎会按照作用域链从内到外查找这个变量。
const color = "blue";
function first() {
const color = "red";
function second() {
// 这里没有声明color变量
console.log(`The color in second() is ${color}`); // 访问first()中的color
}
second();
}
function third() {
console.log(`The color in third() is ${color}`); // 访问全局的color
}
first(); // 输出: The color in second() is red
third(); // 输出: The color in third() is blue
这个例子展示了作用域链查找的过程:
second()
函数内部没有color
变量- 于是查找其外部作用域
first()
,找到了值为"red"的color
third()
函数内部没有color
变量- 于是查找其外部作用域(全局),找到了值为"blue"的
color
词法作用域与变量遮蔽(Shadowing)
当内部作用域中的变量与外部作用域中的变量同名时,内部变量会"遮蔽"外部变量,这种现象称为"变量遮蔽"。
const name = "Global";
function printName() {
const name = "Local"; // 遮蔽了全局的name变量
console.log(name); // 输出: "Local"
}
printName();
console.log(name); // 输出: "Global"
在这个例子中:
- 全局作用域有一个
name
变量 printName()
函数内部也定义了一个name
变量- 当在
printName()
内部访问name
时,会优先使用当前作用域中的name
- 全局的
name
变量被"遮蔽"了,但它仍然存在且未被修改
变量遮蔽可能导致代码难以理解和调试。在实际开发中,应尽量避免在不同作用域中使用相同的变量名。
词法作用域与块级作用域
ES6引入了let
和const
关键字,使JavaScript拥有了真正的块级作用域。块级作用域也遵循词法作用域的规则。
function blockScopeDemo() {
let x = "outer";
if (true) {
let x = "inner"; // 新的块级作用域变量
console.log(x); // 输出: "inner"
}
console.log(x); // 输出: "outer"
}
blockScopeDemo();
块级作用域也会形成词法嵌套关系:
function nestedBlocks() {
let level = "function level";
{
let level = "block level 1";
{
let level = "block level 2";
console.log(`Inside innermost block: ${level}`); // block level 2
}
console.log(`Inside middle block: ${level}`); // block level 1
}
console.log(`Inside function: ${level}`); // function level
}
nestedBlocks();
词法作用域与闭包的关系
词法作用域是理解闭包的基础。闭包是一个函数能够记住并访问其词法作用域的特性,即使当该函数在其原始作用域之外执行时。
function createCounter() {
let count = 0; // 私有变量
return function() {
count++; // 访问词法作用域中的变量
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
在这个例子中:
createCounter
函数创建了一个内部变量count
- 返回的内部函数通过词法作用域能够访问这个
count
变量 - 即使当
createCounter
函数执行完毕,内部函数仍然保持对该词法作用域的访问
这里count
变量对外部代码是不可访问的,形成了一种封装,体现了词法作用域的实际应用价值。
实际应用案例:模块化模式
词法作用域和闭包结合,可以创建私有变量和方法,这是JavaScript模块模式的基础。
const calculatorModule = (function() {
// 私有变量和函数
let result = 0;
function add(x, y) {
return x + y;
}
function multiply(x, y) {
return x * y;
}
// 公开API
return {
addNumbers: function(x, y) {
result = add(x, y);
return result;
},
multiplyNumbers: function(x, y) {
result = multiply(x, y);
return result;
},
getResult: function() {
return result;
}
};
})();
// 使用模块
console.log(calculatorModule.addNumbers(5, 3)); // 8
console.log(calculatorModule.multiplyNumbers(4, 2)); // 8
console.log(calculatorModule.getResult()); // 8
在这个例子中:
- 我们创建了一个立即执行函数表达式(IIFE)
- IIFE内部定义了私有变量
result
和私有函数add
、multiply
- 返回的对象包含公开方法,这些方法可以访问私有变量和函数
- 外部代码无法直接访问私有变量和函数
这就是JavaScript模块化模式的基本实现,它利用了词法作用域和闭包特性来实现信息隐藏和接口暴露。
词法作用域的常见陷阱
1. 变量声明提升
function hoistingExample() {
console.log(x); // undefined (不会报错)
var x = 5;
}
hoistingExample();
使用var
声明的变量会被"提升"到作用域顶部,但初始化不会。使用let
或const
可以避免这个问题:
function letExample() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
}
2. 全局变量意外创建
function globalPollution() {
x = 10; // 没有使用var/let/const声明,创建了全局变量
console.log(x);
}
globalPollution();
console.log(x); // 10 (全局作用域被污染)
3. 循环中的闭包
function createFunctions() {
var funcs = [];
// 有问题的写法
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
return funcs;
}
var functions = createFunctions();
functions[0](); // 3
functions[1](); // 3
functions[2](); // 3
解决方法是使用立即执行函数创建新的作用域,或者使用ES6的let
:
function createFunctionsFixed() {
var funcs = [];
// 使用let修复
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
return funcs;
}
var functions = createFunctionsFixed();
functions[0](); // 0
functions[1](); // 1
functions[2](); // 2
总结
JavaScript的词法作用域是变量访问规则的基础,它决定了代码在各个区域的可见性:
- 词法作用域在代码编写时就已确定(静态作用域)
- 变量查找遵循从内到外的作用域链查找规则
- 内部作用域可以访问外部作用域变量,反之则不行
- 内部作用域可以遮蔽同名的外部变量
- 词法作用域是闭包和模块化模式的基础
掌握词法作用域对于理解JavaScript代码执行、变量访问、以及闭包等高级概念至关重要。合理利用词法作用域规则,可以创建更加模块化、封装性更好的代码。
练习题
-
预测以下代码的输出并解释原因:
javascriptlet x = 10;
function foo() {
console.log(x);
}
function bar() {
let x = 20;
foo();
}
bar(); -
修改以下代码,使每个函数返回对应的索引:
javascriptfunction createButtons() {
var buttons = [];
for (var i = 0; i < 5; i++) {
buttons.push(function() {
console.log('Button ' + i + ' clicked');
});
}
return buttons;
} -
创建一个计数器函数,要求能够增加、减少并获取当前计数,但计数变量不能从外部直接访问。