每日3题(55)

今天只有一道题~

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;
        ...
    }
    
}

猜你喜欢

转载自blog.csdn.net/weixin_44194732/article/details/105923079