let/const for loop 的 v8 实现

作者背景

非 JavaScript 语言律师,(基本)看不懂 ecma-262,C++ 勉强

介绍

Let/const 定义,也就是所谓的 lexical bindings,当出现在 Standard For Loop 的定义中,也就是最常见的 C style for loop,会区别于只带有用 var 定义变量的 for loop。

社区中经典的初学者 for loop 闭包问题也由此而来。

// var standard for loop
(function() {
    let closures = []; 

    for (var i = 0; i < 5; i++) {
        closures.push(() => i); 
    }   

    closures.map(f => console.log(f()));
})();

// let/const standard for loop
(function() {
    let closures = [];

    for (let i = 0; i < 5; i++) {
        closures.push(() => i);
    }

    closures.map(f => console.log(f()));
})();

更多关于这个问题的文章:

v8 实现

Parser::DesugarLexicalBindingsInForStatement 这个函数中,对 let/const standard for loop 进行了 desugar,代码中带有如下注释:

ES6 13.7.4.8 specifies that on each loop iteration the let variables are copied into a new environment.

在每次循环中,let 定义的变量需要一个新的 environment (目测是 ecma-262 里的定义,简单理解成一个新的作用域)。

Moreover, the "next" statement must be evaluated not in the environment of the just completed iteration but in that of the upcoming one.

Standard for loop 中的 next 语句的执行时机/执行环境是下一次循环(的开始),而不是当前这次循环(的结束)。下文会解释。

参见 13.7.4.9 CreatePerIterationEnvironment (FYI: 反正我是没太看懂)

We achieve this with the following desugaring. Extra care is needed to preserve the completion value of the original loop.
We are given a for statement of the form:
labels: for (let/const x = i; cond; next) body
and rewrite it as follows. Here we write {{ ... }} for init-blocks.

desugar 的示意代码我进行了一点改写,应该会更好懂。

for (let/const i = initial_value; cond; next) {
  body 
}

desugar 为:

{
    let temp_i = initial_value;
    let is_first_iteration = true;

    outer_loop: for (;;) {
        let/const i = temp_i;
        {{
            if (is_first_iteration) {
                is_first_iteration = false;
            } else {
                next;
            }
            if (!cond) break;
            should_break = true;
        }}
        labels: for (; should_break == true; should_break = false, temp_i = i) {
            body;
        }
        {{
            // body used/called `break` statement
            if (should_break) break;
        }}
    }
} 

i 作为循环变量,每次循环都会定义一个。cond/next/body 语句里被闭包捕获的变量都是它。

is_first_iteration 这个 flag 是为了实现 next 语句的执行时机/执行环境是下一次循环(的开始),而不是当前这次循环(的结束)。考虑在 next 语句中也存在闭包对 i 进行捕获,执行结果如何?

let closures = [];

for (let i = 0; i < 5; closures.push(() => i)) {
    i++;
}

closures.map(f => console.log(f()))
// 2
// 3
// 4
// 5
// 5

以及:

let closures = [];

for (let i = 0; i < 5; closures.push(() => i), i++) {}

closures.map(f => console.log(f()))
// 1
// 2
// 3
// 4
// 5

至于为什么 ,这就得问标准的作者了...

temp_i 的引入是因为 body 的语句中可能对 i 的值进行了修改,按照 for loop 的语义需要将 i_{n} 的值更新到下一次循环的 i_{n+1} 中。尽管大多数时候我们并不会在 loop 的 cond/next/body 语句中对 i 进行修改。

最后为什么要套两层 loop。看源码目测是为了实现方便,原有的 loop 不用扔,可以直接改写成内部的那个。

当然 v8 也并不会在所有的 const/let for loop 中都使用这样的策略,只有在 loop 的 cond/next/body 语句中存在函数定义或者 eval 的时候才会使用。

结尾

无。

编辑于 2020-12-28 14:48