This article covers ES2017+ runtime system.
丨- Environment
丨- Closure
丨— This
Environment
每个execution context都有一个关联的lexical environment.
Def. 9: Lexical environment: A lexical environment is a structure used to define association between identifiers appearing in the context with their values. Each environment can have a reference to an optional parent environment
所以在一个scope里, environment是变量, 方法, 类的存储空间. 从技术上讲, environment是一组由environment record(将identifiers映射到值的实际存储表)和对父项的引用(可以为空)组成.
例如:
let x = 10;
let y = 20;
function foo(z) {
let x = 100;
return x + y + z;
}
console.log(foo(30)); //150;
从逻辑上讲, 这提醒我们上面讨论过的prototype chain. identifiers resolution的规则非常相似: 如果在自己的environment中找不到变量, 则会尝试在parent environment中, 父级的父级中查找它, 等等 - 直到the whole environment为止.
Def. 10: Identifier resolution: the process of resolving a variable (binding) in an environment chain. An unresolved binding results to ReferenceError
这解释了为什么变量x被解析为100, 但不是10 - 它直接在foo的自己的environment中找到; 为什么我们可以访问参数z - 它也只存储在activation environment中; 以及为什么我们可以访问变量y - 它在parent environment中找到. 与prototypes类似, 几个child environments可以共享相同的parent environment:
例如, 两个global functions共享相同的global environment. environment记录因type而异. 有object environment记录和declarative environment记录. 在declarative记录之上还有function environment记录和module environment记录. 记录的每种类型都只针对它的properties.
但是, identifier resolution的通用机制在所有environments中都很常见, 并且不依赖于记录的类型. object environment记录可以是global environment的记录. 这样的记录也有关联的绑定对象, 它可以存储记录中的一些properties, 但不是其他属性, 反之亦然. 绑定对象也可以作为this
提供.
例如:
var xEnvironment = 10;
let yEnvironment = 20;
console.log(
xEnvironment,
yEnvironment,
)
console.log(
this.xEnvironment,
this.yEnvironment,
)
this['not valid ID'] = 30;
console.log(
this['not valid ID'],
);
请注意, 绑定对象的存在是为了覆盖legacy constructs, 如var
-declarations和with
-statements, 它们也将它们的对象作为绑定对象提供. 当environment被表示为简单对象时, 这些都是历史原因.
目前, environment model更加优化, 但结果我们无法再访问绑定像访问properties. 我们已经看到environment是如何通过父链接相关的. 现在我们将看到一个environment如何outlive创造它的context. 这是我们即将讨论的闭包机制的基础.
Closure
ECMAScript中的函数是一流的. Functions是函数式编程的基础, 它们在JavaScript中被很好地支持.
Def. 11: First-class function: a function which can participate as a normal data: be stored in a variable, passed as an argument, or returned as a value from another function.
与一流的functions的概念相关的是所谓的”Funarg问题”(“A problem of a functional argument”), 它出现在functions不得不处理free variables时.
Def. 12: Free variable: a variable which is neither a parameter, nor a local variable of this function.
让我们来分析下Funarg问题:
例如:
let x = 10;
function foo() {
console.log(x);
}
function bar(funArg) {
let x = 20;
funArg(); // 10, not 20!
}
// Pass `foo` as an argument to `bar`.
bar(foo);
对于函数foo, 变量x是free variables. 当foo函数被激活时(通过funArg参数) - 它应该在哪里解析x的绑定?从创建函数的外部作用域或调用者作用域当函数被调用时?正如我们所看到的, 调用者即bar函数也为x提供了绑定 - 值为20.
上述用例被称为downwards funarg problem(向下漏斗问题), 即在确定绑定的正确环境时的模糊性:它应该是创建时的环境还是被调用时的环境? 这是通过使用static scope的协议解决的, 也就是创建时的scope
Def. 13: Static scope: a language implements static scope, if only by looking at the source code one can determine in which environment a binding is resolved.
static scope(静态作用域)有时也被称为lexical scope(词法作用域), 而后被称为lexical environments. 从技术上讲, static scope是通过捕获function is created的环境来实现的.
Def. 14: Closure: A closure is a function which captures the environment where it’s defined. Further this environment is used for identifier resolution.
在a new fresh activation环境中调用函数, 该环境存储局部变量和参数. the activation environment的parent环境设置为函数的闭包环境, 从而产生the lexical scope语义.
例如:
function fooFoo() {
let x = 10;
function bar() {
return x;
}
return bar;
}
let xFoo = 20;
let barFoo = fooFoo();
barFoo();// 10, not 20!
同样, 从技术上讲, 它与捕获the definition environment的机制没有区别. 就在这种情况下, 如果我们没有关闭, fooFoo的the activation environment将被destroyed.
但是我们捕获了它, 所以它不能被deallocated, 并且被保留 - 以支持static scope语义.
通常对闭包有一个不完全的理解 - 通常开发人员只依照upward的funarg问题来考虑闭包(实际上它确实更有意义).但是, 正如我们所看到的, upward和downwards的funarg问题的技术机制完全相同 - 并且是the static scope的机制.
如上所述, 与prototypes类似, 可以跨多个闭包共享相同的父环境. 这允许访问和改变共享数据,
例如:
function createCounter() {
let count = 0;
return {
increment() {count++; return count;},
decrement() {count--; return count;},
};
}
let counter = createCounter();
console.log(
counter.increment(), //1
counter.decrement(), //0
counter.increment(), //1
);
由于在包含count变量的作用域内创建了闭包 increment和decrement, 因此它们share this parent scope. 也就是说, 捕获总是”by-reference”发生 - 意味着存储了对整个父环境的引用.
某些语言可以捕获”by-value”, making copy of 被捕获的variable, 并且不允许在父作用域中更改它.
但是在Javascript中, 要记得, 它始终是对父作用域的引用. implementations可以优化该步骤, 不捕获整个environment. 捕获used free-variables, 但它们仍然保持parent scopes中可变数据的不变性.
所有identifiers都是statically scoped.但是有一个值在ECMAScript中是dynamically scoped.它就是this
This
TBC