今天只有一道题~
JavaScript:for (let x of [1,2,3]) …:for循环并不比使用函数递归节省开销
JavaScript:for (let x of [1,2,3]) …:for循环并不比使用函数递归节省开销
块
绝大多数 JavaScript 语句都并没有自己的块级作用域。从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
switch语句被设计为有且仅有一个作用域,无论它有多少个 case 语句,其实都是运行在一个块级作用域环境中的。
var x = 100, c = 'a';
switch (c) {
case 'a':
console.log(x); // ReferenceError
break;
case 'b':
let x = 200;
break;
}
因为所有的分支都在同一个块级作用域中,let具有暂时性死区,在没有声明前使用会报错。如果将let x = 200
换成var x = 200
当然就没有问题。
一些简单的、显而易见的块级作用域包括:
// 例1
try {
// 作用域1
}
catch (e) { // 表达式e位于作用域2
// 作用域2
}
finally {
// 作用域3
}
// 例2
//(注:没有使用大括号)
with (x) /* 作用域1 */; // <- 这里存在一个块级作用域
// 例3, 块语句
{
// 作用域1
除了这三个语句和 一个特例(今天的title) 之外,所有其它的语句都是没有块级作用域的。例如if条件语句的几种常见书写形式:
if (x) {
...
}
// or
if (x) {
...
}
else {
...
}
这些语法中的“块级作用域”都是一对大括号表示的“块语句”自带的,而与if语句本身无关。
第二个作用域
var x = 100;
for (let x = 102; x < 105; x++)
console.log('value:', x); // 显示“value: 102~104”
console.log('outer:', x); // 显示“outer: 100”
因为for语句的这个块级作用域的存在,导致循环体内访问了一个局部的x值(循环变量),而外部的(outer)变量x是不受影响的。
for (let x = 102; x < 105; x++)
let x = 200;
如果循环体(单个语句)允许支持新的变量声明,那么为了避免它影响到循环变量,就必须为它再提供另一个块级作用域。很有趣的是,在这里,JavaScript 是不允许声明新的变量的。
但是这里有一个疑问,加上大括号就会没有问题,但是作者在评论又说{}并没有“强制创建作用域”这样的能力。
,或许{}只是通知for循环的第二个作用域准备开启了。已解决:let遇见{}会显试的创建块级作用域。
下面是禁例的语法
// if语句中的禁例
if (false) let x = 100;
// while语句中的禁例
while (false) let x = 200;
// with语句中的禁例
with (0) let x = 300
所以,现在可以确定:循环语句(对于支持“let/const”的 for 语句来说)“通常情况下”只支持一个块级作用域。
但是如果在 for 语句支持了 let/const 的情况下,仅仅只有一个块级作用域是不方便的。例如:
for (let i=0; i<2; i++) /* 用户代码 */;
在这个例子中,“只有一个块级作用域”的设计,将会导致“用户代码”直接运行在与“let 声明”相同的词法作用域中。也就是说let i = 0只执行了一次。
但对于下面这个例子
for (let i in x) ...;
let i在语义上被执行很多次,这就与**“let/const”语句的单次声明**(不可覆盖)的设计,与迭代多次执行的现实逻辑矛盾了。
这个矛盾的起点,就是“只有一个块级作用域,一个块级作用域中不可以重复声明let/const的变量”。所以,在 JavaScript 引擎实现“支持 let/const 的 for 语句”时,就在这个地方做了特殊处理:为循环体增加一个作用域。
for 循环的代价
在 JavaScript 的具体执行过程中,作用域是被作为环境的上下文来创建的。如果将 for 语句的块级作用域称为 forEnv,并将上述为循环体增加的作用域称为loopEnv,那么 **loopEnv **它的外部环境就指向 forEnv。
上面矛盾貌似被解决了,下一个例子
for (let i in x)
setTimeout(()=>console.log(i), 1000);
这个例子创建了一些定时器。当定时器被触发时,函数会通过它的闭包(这些闭包处于 loopEnv 的子级环境中)来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个 for 迭代有可能都已经结束了。这种情况下,要么上面的 forEnv 已经没有了、被销毁了,要么它即使存在,那个i的值也已经变成了最后一次迭代的终值。
所以,要想使上面的代码符合预期,这个 loopEnv 就必须是“随每次迭代变化的”。也就是说,需要为每次迭代都创建一个新的作用域副本,这称为迭代环境(iterationEnv)。因此,每次迭代在实际上都并不是运行在 loopEnv 中,而是运行在该次迭代自有的 iterationEnv 中。
也就是说,在语法上这里只需要两个“块级作用域”,而实际运行时却需要为其中的第二个块级作用域创建无数个副本。
这就是 for 语句中使用“let/const”这种块级作用域声明所需要付出的代价。
但是不明白的是对于forEnv和loopEnv的范围又是在哪里?let i的声明在哪里?
我的理解:
// 类型1
for (let i=0; i<2; i++){ /* 用户代码 */};
{// forEnv
let i = 0;
{ //loopEnv-> iterationEnv1
i = 1
...
}
{ //loopEnv-> iterationEnv2
i = 2
...
}
}
// 类型2
for (let i in x){...}
{// forEnv
{ //loopEnv-> iterationEnv1
let i = 1;
...
}
{ //loopEnv-> iterationEnv2
let i = 2;
...
}
{ //loopEnv-> iterationEnv3
let i = 3;
...
}
{ //loopEnv-> iterationEnvn
let i = n;
...
}
}