[Javascript] The Core (2)

sddtc 于 2018-07-18 发布

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 environmentparent环境设置为函数的闭包环境, 从而产生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问题来考虑闭包(实际上它确实更有意义).但是, 正如我们所看到的, upwarddownwards的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变量的作用域内创建了闭包 incrementdecrement, 因此它们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