全局和局部变量范围

在本文中,您将了解作用域及其在 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);
}

范围类型

全局范围

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

假设有一个 HTML 文件导入了两个 JavaScript 文件: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 关键字来声明初始化器变量。

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 循环中修复,在第二个 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 开发者,但了解作用域的工作原理有助于您在出现 bug 时修复问题。