2023年7月30日

变量作用域,闭包

JavaScript 是一种非常面向函数的语言。它给了我们很大的自由度。在 JavaScript 中,我们可以随时创建函数,可以将函数作为参数传递给另一个函数,并在完全不同的代码位置进行调用。

我们已经知道函数可以访问其外部的变量。

但是,如果在函数被创建之后,外部变量发生了变化会怎样?函数会获得新值还是旧值?

如果将函数作为参数(argument)传递并在代码中的另一个位置调用它,该函数将访问的是新位置的外部变量吗?

让我们通过本文来学习这些相关知识,以了解在这些场景以及更复杂的场景下到底会发生什么。

我们将在这探讨一下 let/const

在 JavaScript 中,有三种声明变量的方式:letconst(现代方式),var(过去留下来的方式)。

  • 在本文的示例中,我们将使用 let 声明变量。
  • const 声明的变量的行为也相同(译注:与 let 在作用域等特性上是相同的),因此,本文也涉及用 const 进行变量声明。
  • 旧的 var 与上面两个有着明显的区别,我们将在 老旧的 "var" 中详细介绍。

代码块

如果在代码块 {...} 内声明了一个变量,那么这个变量只在该代码块内可见。

例如:

{
  // 使用在代码块外不可见的局部变量做一些工作

  let message = "Hello"; // 只在此代码块内可见

  alert(message); // Hello
}

alert(message); // Error: message is not defined

我们可以使用它来隔离一段代码,该段代码执行自己的任务,并使用仅属于自己的变量:

{
  // 显示 message
  let message = "Hello";
  alert(message);
}

{
  // 显示另一个 message
  let message = "Goodbye";
  alert(message);
}
这里如果没有代码块则会报错

请注意,如果我们使用 let 对已存在的变量进行重复声明,如果对应的变量没有单独的代码块,则会出现错误:

// 显示 message
let message = "Hello";
alert(message);

// 显示另一个 message
let message = "Goodbye"; // Error: variable already declared
alert(message);

对于 ifforwhile 等,在 {...} 中声明的变量也仅在内部可见:

if (true) {
  let phrase = "Hello!";

  alert(phrase); // Hello!
}

alert(phrase); // Error, no such variable!

在这儿,当 if 执行完毕,则下面的 alert 将看不到 phrase,因此会出现错误。(译注:就算下面的 alert 想在 if 没执行完成时去取 phrase(虽然这种情况不可能发生)也是取不到的,因为 let 声明的变量在代码块外不可见。)

太好了,因为这就允许我们创建特定于 if 分支的块级局部变量。

对于 forwhile 循环也是如此:

for (let i = 0; i < 3; i++) {
  // 变量 i 仅在这个 for 循环的内部可见
  alert(i); // 0,然后是 1,然后是 2
}

alert(i); // Error, no such variable

从视觉上看,let i 位于 {...} 之外。但是 for 构造在这里很特殊:在其中声明的变量被视为块的一部分。

嵌套函数

如果一个函数是在另一个函数中创建的,该函数就被称为“嵌套”函数。

在 JavaScript 中很容易实现这一点。

我们可以使用嵌套来组织代码,比如这样:

function sayHiBye(firstName, lastName) {

  // 辅助嵌套函数使用如下
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );

}

这里创建的 嵌套 函数 getFullName() 是为了更加方便。它可以访问外部变量,因此可以返回全名。嵌套函数在 JavaScript 中很常见。

更有意思的是,可以返回一个嵌套函数:作为一个新对象的属性或作为结果返回。之后可以在其他地方使用。不论在哪里调用,它仍然可以访问相同的外部变量。

下面的 makeCounter 创建了一个 “counter” 函数,该函数在每次调用时返回下一个数字:

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

尽管很简单,但稍加变型就具有很强的实际用途,比如,用作 随机数生成器 以生成用于自动化测试的随机数值。

这是如何运作的呢?如果我们创建多个计数器,它们会是独立的吗?这里的变量是怎么回事?

理解这些内容对于掌握 JavaScript 的整体知识很有帮助,并且对于应对更复杂的场景也很有益处。因此,让我们继续深入探究。

词法环境

前方高能!

一大波深入的技术讲解即将到来。

尽管我很想避免编程语言的一些底层细节,但是如果没有它们,我们就无法完整地理解词法作用域,所以我们这就开始吧!

为了使内容更清晰,这里将分步骤进行讲解。

Step 1. 变量

在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

词法环境对象由两部分组成:

  1. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
  2. 外部词法环境 的引用,与外部代码相关联。