在本章中,我们将深入探讨 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
时,都会从堆栈中删除一个执行上下文。
我们使用以下代码来探讨如何通过环境实现嵌套作用域。
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
语句会将执行条目从堆栈中弹出。为了了解如何使用环境来实现闭包,我们使用以下示例:
这里发生了什么?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);
会发生以下情况: