全局和局部变量范围

在本文中,您将了解范围及其在 JavaScript 中的工作原理。

作用域是 JavaScript 和其他编程语言中的基本概念,用于定义访问和使用变量的上下文。随着您不断学习 JavaScript 和使用变量,它变得更加实用,也适用于您的代码。

借助范围,您可以:

  • 更高效地使用内存:作用域能够仅在需要时加载变量。如果某个变量不在范围内,您无需使其可供当前正在执行的代码使用。
  • 更轻松地查找和修复 bug:使用局部作用域隔离变量可以更轻松地对代码中的 bug 进行问题排查,因为与全局变量不同,您可以信任来自外部作用域的代码无法操纵局部范围的变量。
  • 创建小型可重用代码块:例如,您可以编写一个不依赖于外部范围的纯函数。您只需进行极少的更改即可轻松将此类函数移动到其他位置。

范围是什么?

变量的范围决定了您可以在代码中的哪个位置使用变量。

JavaScript 定义全局或局部范围的变量:

  • 具有全局范围的变量可通过 JavaScript 代码中的所有其他范围使用。
  • 具有局部作用域的变量仅在特定的本地上下文中可用,并且由关键字(例如 varletconst)创建。如果您使用 varletconst 关键字在函数内创建变量,则该变量具有局部范围。

本文后面的部分将讨论块和词法范围:

  • 块范围变量仅在块本地可用,具体取决于定义块语句的大括号位置。只有使用 letconst 关键字声明的变量才具有块范围。
  • 词法作用域根据源代码中声明变量的位置来确定该变量在哪里可用。您可以使用闭包向封闭函数授予对外部范围(称为词法环境)中引用的变量的访问权限。

在其范围内访问变量时,JavaScript 会返回为其赋值的值,或以其他方式产生错误。

如需声明变量,请执行以下操作:

  • 使用 varconstlet 关键字声明局部或全局范围的变量。
  • 使用 constlet 关键字来声明块级变量。

如果在函数中声明 var 变量,该声明会使该变量可供最接近的封闭函数使用。不能使用 var 关键字来声明块范围的变量。

范围示例

此示例演示了全局范围,因为 greeting 变量在任何函数或块之外声明,这使得它的值可供当前文档中的所有代码使用:

const greeting = 'hello';
console.log(greeting); // 'hello'

在全局范围示例中,为 greeting 变量赋了 hello 值。

此示例演示了局部范围,因为它在函数中使用 let 关键字声明了 greeting 变量。greeting 变量是局部范围的变量,在函数外部不可用。

function greet() {
  let greeting = 'Hello World!';
  console.log(greeting);
}

此示例演示了块作用域,因为它在块中声明了 greeting 变量,因此只能在大括号内访问该变量:

if (true) {
   const greeting = 'hello';
}

console.log(greeting); // ReferenceError: greeting is not defined

请注意,当 console.log 函数尝试输出 greeting 变量的值时,JavaScript 会返回 ReferenceError 错误消息,而不是预期的 hello 消息。原因何在?

由于 greeting 变量具有块范围,而最近的块是 if 条件语句的一部分,因此系统会返回一个错误。您无法从块外部访问您在块内声明的 letconst 变量。因此,您只能访问大括号内用于指定块范围的 greeting 变量。

此示例修复了该错误,因为它将 console.log(message) 方法移到了大括号内。更新后的代码会在代码块内重新调整 console.log(message) 方法的位置。

if (true) {
   const greeting = 'hello';
   console.log(greeting);
}

范围类型

全局范围

您可以从程序中的任何位置访问具有全局范围的变量。

假设有一个导入以下两个 JavaScript 文件的 HTML 文件:file-1.jsfile-2.js

<script src="file-1.js"></script>
<script src="file-2.js"></script>

在此示例中,globalMessage 变量具有全局范围,并且是在函数外部编写的。在运行和执行期间,您可以从 JavaScript 程序中的任何位置访问 globalMessage 变量的值。

您可以在以下代码段中查看 file-1.jsfile-2.js 文件的内容。请注意 globalMessage 变量在两个文件中的可用性。

// file-1.js
function hello() {
    var localMessage = 'Hello!';
}

var globalMessage = 'Hey there!';

// file-2.js
console.log(localMessage); // localMessage is not defined
console.log(globalMessage); // Hey there!

还有一种类型的范围,本文未详细讨论。如果您在 JavaScript 模块内(但在函数或块之外)创建变量,则该变量的作用域不是全局,而是模块作用域。具有模块范围的变量可在当前模块中的任何位置使用,但无法在其他文件或模块中使用。如需使模块级范围的变量可供其他文件使用,您必须从创建该变量的模块中导出该变量,然后从需要访问该变量的模块中import该变量。

局部范围和函数范围

如果您使用 varletconst 关键字在 JavaScript 函数中创建变量,这些变量是函数的本地变量,因此您只能从函数内访问它们。局部变量在函数开始时创建,在函数执行结束时实际删除。

此示例在 addNumbers() 函数中声明了 total 变量。您只能访问 addNumbers() 函数中的 ab,total 变量。

function addNumbers(a, b) {
    const total = a + b;
}

addNumbers(3, 4);

您可以使用 letconst 关键字为变量命名。当您使用 let 关键字时,JavaScript 可以更新该变量。不过,使用 const 关键字时,该变量保持不变。

var variable1 = 'Declared with var';
var variable1 = 'Redeclared with var';
variable1; // Redeclared with var

let variable2 = 'Declared with let. Cannot be redeclared.';
variable2 = 'let cannot be redeclared, but can be updated';
variable2; // let cannot be redeclared, but can be updated

const variable3 = 'Declared with const. Cannot be redeclared or updated';
variable3; // Declared with const. Cannot be redeclared or updated

屏蔽范围

用于将单个语句或一组语句组合在一起。您可以使用 constlet 关键字来声明块范围的局部变量。请注意,不能使用 var 关键字来声明块范围的变量。

例如,在下面的代码块中,name 变量的作用域及其 "Elizabeth" 值就包含在大括号内。块作用域内的变量在块之外不可用。

{
    const name = "Elizabeth";
}

您可以在 ifforwhile 语句中使用块级范围的变量。

请注意以下代码段中的两个 for 循环。一个 for 循环使用 var 关键字声明初始化变量,该变量按数字 012 递增。另一个 for 循环使用 let 关键字来声明 initializer 变量。

for (var i = 0; i < 2; i++) {
    // ...
}

console.log(i); // 2

for (let j = 0; j < 2; j++) {
    // ...
}

console.log(j); // The j variable isn't defined.

在前面的代码示例中,您可能会注意到,第一个 for 循环中的 i 变量在 for 循环之外泄露,并且仍然保留了 2 值,因为 var 关键字不使用块级范围。此问题已在第二个 for 循环中得到修复,其中使用 let 关键字声明的 j 变量的作用域限定为 for 循环的块,在 for 循环结束后不存在。

在其他范围内重复使用变量名称

即使在不同作用域内的其他位置重复使用同一变量名称,作用域也能隔离函数内的变量。

以下示例展示了如何使用作用域在不同函数中重复使用同一变量名称:

function listOne() {
    let listItems = 10;
    console.log(listItems); // 10
}

function listTwo() {
   let listItems = 20;
   console.log(listItems); // 20
}

listOne();
listTwo();

系统会为 listOne()listTwo() 函数中的 listItems 变量分配预期值,因此不会相互冲突。

闭包和词法范围

闭包是指封闭函数,其中内部函数可以访问外部函数作用域,这也称为词法环境。因此,在 JavaScript 中,您使用闭包让函数引用外部词法环境,函数内的代码可以引用函数外部声明的变量。事实上,您可以编写指向外部词法环境的引用链,这样,一个函数就会被一个函数调用,而另一个函数又会调用该函数。

在此示例中,代码使用在调用 outer() 函数(关闭 hello 变量)时创建的词法环境形成闭包。因此,hello 变量用在 setTimeout 回调函数中。

function outer() {
    const hello = 'world';

    setTimeout(function () {
        console.log('Within the closure!', hello)
    }, 100);
}

outer();

使用词法作用域时,作用域是在编译源代码期间(而不是在运行时)确定的。如需详细了解词法环境,请参阅词法范围限定和闭包

单元

JavaScript 模块有助于整理 JavaScript 代码。如果使用得当,它们可为代码库提供有效的结构,并有助于重复使用代码。JavaScript 模块提供了一种导出import变量的方法,而不是使用全局变量在不同文件之间共享变量。

// hello.js file
function hello() {
  return 'Hello world!';
}

export { hello };

// app.js file
import { hello } from './hello.js';

console.log(hello()); // Hello world!

作用域可视化工具演示

作用域是每个 JavaScript 开发者都应该理解的基本概念。为了更好地了解作用域系统,您可以尝试使用 JS 作用域可视化工具编写自己的代码。此演示在代码中使用颜色来帮助您直观呈现 JavaScript 作用域。

总结

本文介绍了不同类型的范围。JavaScript 作用域是 Web 开发中更高级的概念之一,因此您应该已通读此内容并花时间了解了这个主题。

范围不是面向用户的功能。它只会影响编写代码的 Web 开发者,但了解作用域的运作方式可以帮助您修复出现的 bug。