跳到主要内容

JavaScript 词法作用域

什么是词法作用域?

词法作用域(Lexical Scope)是JavaScript中变量可见性和访问权限的一种规则体系。"词法"一词来源于这种作用域是在代码编写阶段(即词法分析时)就已确定,而不是在运行时确定的特点。

简单来说,词法作用域决定了代码中的变量在哪些区域可以被访问,在哪些区域不能被访问。它遵循一个基本原则:内部作用域可以访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。

关键点

词法作用域也被称为"静态作用域",因为它在代码编写时(静态分析阶段)就已经确定,与代码如何执行无关。

词法作用域的基本规则

JavaScript的词法作用域遵循以下基本规则:

  1. 查找变量时,先在当前作用域查找
  2. 如果当前作用域没有找到,则向外层作用域查找
  3. 逐层向外查找,直到全局作用域
  4. 如果全局作用域也没有,则返回undefined或抛出ReferenceError

让我们用代码来看一个简单的例子:

javascript
// 全局作用域
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引擎会按照作用域链从内到外查找这个变量。

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

这个例子展示了作用域链查找的过程:

  1. second()函数内部没有color变量
  2. 于是查找其外部作用域first(),找到了值为"red"的color
  3. third()函数内部没有color变量
  4. 于是查找其外部作用域(全局),找到了值为"blue"的color

词法作用域与变量遮蔽(Shadowing)

当内部作用域中的变量与外部作用域中的变量同名时,内部变量会"遮蔽"外部变量,这种现象称为"变量遮蔽"。

javascript
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引入了letconst关键字,使JavaScript拥有了真正的块级作用域。块级作用域也遵循词法作用域的规则。

javascript
function blockScopeDemo() {
let x = "outer";

if (true) {
let x = "inner"; // 新的块级作用域变量
console.log(x); // 输出: "inner"
}

console.log(x); // 输出: "outer"
}

blockScopeDemo();

块级作用域也会形成词法嵌套关系:

javascript
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();

词法作用域与闭包的关系

词法作用域是理解闭包的基础。闭包是一个函数能够记住并访问其词法作用域的特性,即使当该函数在其原始作用域之外执行时。

javascript
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

在这个例子中:

  1. createCounter函数创建了一个内部变量count
  2. 返回的内部函数通过词法作用域能够访问这个count变量
  3. 即使当createCounter函数执行完毕,内部函数仍然保持对该词法作用域的访问

这里count变量对外部代码是不可访问的,形成了一种封装,体现了词法作用域的实际应用价值。

实际应用案例:模块化模式

词法作用域和闭包结合,可以创建私有变量和方法,这是JavaScript模块模式的基础。

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和私有函数addmultiply
  • 返回的对象包含公开方法,这些方法可以访问私有变量和函数
  • 外部代码无法直接访问私有变量和函数

这就是JavaScript模块化模式的基本实现,它利用了词法作用域和闭包特性来实现信息隐藏和接口暴露。

词法作用域的常见陷阱

1. 变量声明提升

javascript
function hoistingExample() {
console.log(x); // undefined (不会报错)
var x = 5;
}

hoistingExample();

使用var声明的变量会被"提升"到作用域顶部,但初始化不会。使用letconst可以避免这个问题:

javascript
function letExample() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
}

2. 全局变量意外创建

javascript
function globalPollution() {
x = 10; // 没有使用var/let/const声明,创建了全局变量
console.log(x);
}

globalPollution();
console.log(x); // 10 (全局作用域被污染)

3. 循环中的闭包

javascript
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

javascript
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的词法作用域是变量访问规则的基础,它决定了代码在各个区域的可见性:

  1. 词法作用域在代码编写时就已确定(静态作用域)
  2. 变量查找遵循从内到外的作用域链查找规则
  3. 内部作用域可以访问外部作用域变量,反之则不行
  4. 内部作用域可以遮蔽同名的外部变量
  5. 词法作用域是闭包和模块化模式的基础

掌握词法作用域对于理解JavaScript代码执行、变量访问、以及闭包等高级概念至关重要。合理利用词法作用域规则,可以创建更加模块化、封装性更好的代码。

练习题

  1. 预测以下代码的输出并解释原因:

    javascript
    let x = 10;

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

    function bar() {
    let x = 20;
    foo();
    }

    bar();
  2. 修改以下代码,使每个函数返回对应的索引:

    javascript
    function createButtons() {
    var buttons = [];
    for (var i = 0; i < 5; i++) {
    buttons.push(function() {
    console.log('Button ' + i + ' clicked');
    });
    }
    return buttons;
    }
  3. 创建一个计数器函数,要求能够增加、减少并获取当前计数,但计数变量不能从外部直接访问。

进一步学习资源