在本章中,我们将深入探讨 ECMAScript 语言规范如何处理变量。
环境是 ECMAScript 规范用来管理变量的数据结构。它是一个字典,其键是变量名,其值是这些变量的值。每个作用域都有其关联的环境。环境必须能够支持与变量相关的以下现象:
我们将使用示例来说明如何针对每种现象实现这一点。
我们先来解决递归问题。请看以下代码:
function f(x) {
return x * 2;
}
function g(y) {
const tmp = y + 1;
return f(tmp);
}
assert.equal(g(3), 8);
对于每个函数调用,您都需要为被调用函数的变量(参数和局部变量)提供新的存储空间。这是通过所谓的*执行上下文*堆栈来管理的,执行上下文是对环境的引用(就本章而言)。环境本身存储在堆上。这是必要的,因为它们偶尔会在执行离开其作用域后继续存在(我们将在探索*闭包*时看到这一点)。因此,它们本身不能通过堆栈来管理。
在执行代码时,我们会在以下位置暂停:
function f(x) {
// Pause 3
return x * 2;
}
function g(y) {
const tmp = y + 1;
// Pause 2
return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);
会发生以下情况:
暂停 1 – 在调用 g()
之前(图 1)。
暂停 2 – 在执行 g()
时(图 2)。
暂停 3 – 在执行 f()
时(图 3)。
剩余步骤:每次出现 return
时,都会从堆栈中删除一个执行上下文。
g()
之前:执行上下文堆栈有一个条目,指向顶级环境。在该环境中,有两个条目;一个用于 f()
,一个用于 g()
。g()
时:执行上下文堆栈的顶部指向为 g()
创建的环境。该环境包含参数 y
和局部变量 tmp
的条目。f()
时:现在,顶部执行上下文指向 f()
的环境。我们使用以下代码来探讨如何通过环境实现嵌套作用域。
function f(x) {
function square() {
const result = x * x;
return result;
}
return square();
}
assert.equal(f(6), 36);
这里,我们有三个嵌套的作用域:顶级作用域、f()
的作用域和 square()
的作用域。观察结果:
因此,每个作用域的环境都通过一个名为 outer
的字段指向外围作用域的环境。当我们在查找变量的值时,我们首先在当前环境中搜索其名称,然后在外围环境中搜索,然后在外围环境的外围环境中搜索,依此类推。整个外围环境链包含当前可以访问的所有变量(减去被遮蔽的变量)。
当您进行函数调用时,您会创建一个新的环境。该环境的外围环境是创建该函数的环境。为了帮助设置通过函数调用创建的环境的 outer
字段,每个函数都有一个名为 [[Scope]]
的内部属性,该属性指向其“诞生环境”。
这些是我们执行代码时所做的暂停:
function f(x) {
function square() {
const result = x * x;
// Pause 3
return result;
}
// Pause 2
return square();
}
// Pause 1
assert.equal(f(6), 36);
会发生以下情况:
f()
之前(图 4)。f()
时(图 5)。square()
时(图 6)。return
语句会将执行条目从堆栈中弹出。f()
之前:顶级环境有一个条目,用于 f()
。f()
的诞生环境是顶级环境。因此,f
的 [[Scope]]
指向它。f()
时:现在有一个用于函数调用 f(6)
的环境。该环境的外围环境是 f()
的诞生环境(索引 0 处的顶级环境)。我们可以看到 outer
字段被设置为 f
的 [[Scope]]
的值。此外,新函数 square()
的 [[Scope]]
是刚刚创建的环境。square()
时:重复了之前的模式:最新环境的 outer
是通过我们刚刚调用的函数的 [[Scope]]
设置的。通过 outer
创建的作用域链包含当前处于活动状态的所有变量。例如,如果我们想访问 result
、square
和 f
,我们可以这样做。环境反映了变量的两个方面。首先,外围环境链反映了嵌套的静态作用域。其次,执行上下文堆栈反映了动态地进行了哪些函数调用。为了了解如何使用环境来实现闭包,我们使用以下示例:
这里发生了什么?add()
是一个返回函数的函数。当我们在 B 行进行嵌套函数调用 add(3)(1)
时,第一个参数用于 add()
,第二个参数用于它返回的函数。这是有效的,因为在 A 行创建的函数在离开其作用域时不会丢失与其诞生作用域的连接。关联的环境通过该连接保持活动状态,并且该函数仍然可以访问该环境中的变量 x
(x
在函数内部是自由的)。
这种嵌套调用 add()
的方式有一个优点:如果您只进行第一次函数调用,您将获得一个 add()
的版本,其参数 x
已经填写完毕:
将具有两个参数的函数转换为两个嵌套的函数(每个函数有一个参数)称为*柯里化*。add()
是一个柯里化函数。
仅填写函数的部分参数称为*偏函数应用*(该函数尚未完全应用)。函数的 .bind()
方法执行偏函数应用。在前面的示例中,我们可以看到,如果一个函数是柯里化的,那么偏函数应用就很简单。
在执行以下代码时,我们做了三次暂停:
function add(x) {
return (y) => {
// Pause 3: plus2(5)
return x + y;
}; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);
会发生以下情况:
add(2)
期间:我们可以看到 add()
返回的函数已经存在(见右下角),并且它通过其内部属性 [[Scope]]
指向其诞生环境。请注意,plus2
仍处于其时间死区,尚未初始化。add(2)
之后:plus2
现在指向 add(2)
返回的函数。该函数通过其 [[Scope]]
保持其诞生环境(add(2)
的环境)处于活动状态。plus2(5)
时:plus2
的 [[Scope]]
用于设置新环境的 outer
。这就是当前函数如何访问 x
的方式。