JavaScript 是一种非常面向函数的语言。它给了我们很大的自由度。在 JavaScript 中,我们可以随时创建函数,可以将函数作为参数传递给另一个函数,并在完全不同的代码位置进行调用。
我们已经知道函数可以访问其外部的变量。
但是,如果在函数被创建之后,外部变量发生了变化会怎样?函数会获得新值还是旧值?
如果将函数作为参数(argument)传递并在代码中的另一个位置调用它,该函数将访问的是新位置的外部变量吗?
让我们通过本文来学习这些相关知识,以了解在这些场景以及更复杂的场景下到底会发生什么。
let/const在 JavaScript 中,有三种声明变量的方式:let,const(现代方式),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);
对于 if,for 和 while 等,在 {...} 中声明的变量也仅在内部可见:
if (true) {
let phrase = "Hello!";
alert(phrase); // Hello!
}
alert(phrase); // Error, no such variable!
在这儿,当 if 执行完毕,则下面的 alert 将看不到 phrase,因此会出现错误。(译注:就算下面的 alert 想在 if 没执行完成时去取 phrase(虽然这种情况不可能发生)也是取不到的,因为 let 声明的变量在代码块外不可见。)
太好了,因为这就允许我们创建特定于 if 分支的块级局部变量。
对于 for 和 while 循环也是如此:
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) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
- 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this的值)的对象。 - 对 外部词法环境 的引用,与外部代码相关联。