05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销
05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销
讲述:周爱民
时长19:39大小17.99M
块
循环语句中的块
特例
特例中的特例
第二个作用域
for 循环的代价
知识回顾
思考题
赞 9
提建议
精选留言(59)
- Y2019-11-20老师,在es6中,其实for只要写大括号就代表着块级作用域。所以只要写大括号,不管用let 还是 var,一定是会创建相应循环数量的块级作用域的。 如果不用大括号,在for中使用了let,也会创建相应循环数量的块级作用域。 也就是说,可以提高性能的唯一情况只有(符合业务逻辑的情况下),循环体是单行语句就不使用大括号且for中使用var。
作者回复: 是的。 赞,好几个赞。^^.
共 13 条评论41 - westfall2019-11-20因为单语句没有块级作用域,而词法声明是不可覆盖的,单语句后面的词法声明会存在潜在的冲突。
作者回复: :) +1
共 2 条评论20 - 小毛2020-03-05老师,最后的思考题感觉有点懵,按你文章里说的,for(let/const...)这中语法,不管怎样在执行阶段,都会为每次循环创建一个iterationEnv块级作用域,那又为什么在单语句语法中不能有let词法声明呢,像if不能有是可以理解的,但是对于for(let/const...)就不能理解了。 另外如果要提高for的性能,是不是不for(let/const...)这样写,把let x放在for语句体外,在其之前声明,是不是就可以在执行阶段只有一个loopEnv,而不创建iterationEnv,从而提高性能。展开
作者回复: 这个问题点问得非常细,解释起来也不容易。 > 1. for (let/const x...) 在这个结构中,for语句总是会生成一个块级作用域,用来放x等等变量。这一点是没有疑问的。并且,由于var声明,或者没有任何声明的for语句在这里不需要放变量名,所以在那些语法格式,也就不产生块级作用域。这个理由和逻辑也很清晰。这个地方创建的作用域(环境)称为forEnv。 基于此,我们继续讨论。 > 2. for (let/const x...) ... 在第二个...位置,亦即是forBody的位置如果没有块语句,那么这里就会被识别为“单语句上下文(single-statement context)”,也就是说这种情况下for被理解为单语句。if语句在这里的情况也一样,也是没有块语句,就理解为单语句。 对于forBody来说,它每一次循环都需要创建一个iterationEnv,这个iterationEnv抄写自loopEnv。——注意这里是抄写,而不是简单地将parent指向loopEnv,所以它确实比较消耗资源。(再次说明,loopEnv的parent指向forEnv,但iterationEnv是抄写loopEnv而不是指向它)。 但为什么要“抄写”呢?这个部分在正文里面有仔细讲,使用了一个基于setTimeout()的例子,请再回顾一下。 但是上面(在这个评论的)第1部分中说到的单语句的部分为什么仍然要iterationEnv呢?——这个才是你的问题本身不是? 其实这就与单语句或块语句无关了。for语句是不包括后面的大括号的。它的语法就是`for (...) ...`,后面是大括号还是单语句上下文,无关。所以`for (let/const ...) ...`语句就约定了每次循环都创建iterationEnv并抄写自loopEnv,以确保在forBody部分可以创建新的作用域,而至于在forBody中是setTimeout打开中的函数闭包,还是一个块语句,它们的处理逻辑(以及对块级作用域的需求)其实都是一样的。 最后,你的问题提到是不是可以将let x放for语句外。是的,这会提高效率,并且也不需要创建loopEnv和iterationEnv。你也可以考虑用var,以及用一个函数来包起来,避免变量泄露到全局。简单地说,使用函数内套一个for循环,并在函数内管理变量名,比将这些变量名放到for (let/const ...)循环语句中,要效率高一些。 如上。
14 - Elmer2019-12-08从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。 单语句如果支持变量声明,相当于需要支持为iteration env新增子作用域,降低了效率? 如果需要完全可以自己写{} 来强制生成一个子作用域 不知道这样说对不对
作者回复: 正是如此😃👍
12 - wDaLian2020-01-12const array = Array.from({length:100000},(item,i)=>i) // 案例一 console.time('a') const cc = [] for(let i in array){ cc.push(i) } console.log(cc) console.timeEnd('a') // 案例二 console.time('b') const ccc = [] for(var i in array){ ccc.push(i) } console.log(ccc) console.timeEnd('b') // 案例三 console.time('c') const cccv = [] for(let i in array) cccv.push(i); console.log(cccv) console.timeEnd('c') 1.老师你上次的评论我没看懂,第一我案例一和案例三是为了做区分所以案例一有大括号的 2.编译引擎的debug版本然后track内核,或者你可以尝试一个prepack-core这个项目,这两个东西是啥 我百度也没查到 3.老师你讲的都是概念的,我就想看到一个肉眼的案例然后根据概念消化,要不现在根本就是这个for循环到底应该咋写我都懵了展开
作者回复: 很晚才回复你的这个问题,原因是确实不好回复,不知道哪种方法才能有效地解决你的疑惑。 首先,不要相信你写的代码,它并不是最终执行的,引擎会做一些优化,这些优化不是语言本身的,所以也不适用于我们在这个课程中所讨论的。 其次,如果你需要用你所列举的类似代码来(粗略地)检查性能,那么建议把数量提高100~1000倍以上,我运行了你的代码,单个测试case大概才20ms,这种情况下,随便的一个后台进程的波动就影响了结果,有效性成问题。再一次强调,不要用这种方法来检测性能,不要相信你的代码在“字面上的表现出来的”效率。 第三,关于debug版本并track内核,我建议你参考一下下面这两篇,一篇是讲编译的,一篇是讲优化的: ``` https://zhuanlan.zhihu.com/p/25120909 https://segmentfault.com/a/1190000008285728 ``` 我原来的意思是说,你可能会在原生语言(例如C)这个层面调试和分析内核有困难,所以就向你推荐了一下prepack-core。这个也是一个js引擎,但是是用javascript写的,你分析起来会好一些。——但坦率地说,也并不容易,这个项目还是很难的。在这里: ``` https://github.com/aimingoo/prepack-core ``` 第四,我认为我还是应该给你一个简单的分析路径,来解释你的问题。从你的代码来看,你只是想尝试for let/var两种语法到底性能上有什么样的差异。我的建议是这样: ``` var array = Array.from({length:10},(item,i)=>i); // 例1 var a = [], checker1 = ()=>console.log(a[1] == a[5]); // anything for (var i in array) setTimeout(()=>a.push(i), 0); setTimeout(checker1, 0); // true // 例2 var b = [], checker2 = ()=>console.log(b[1] == b[5]); // anything for (let i in array) setTimeout(()=>b.push(i), 0); setTimeout(checker2, 0); // false ``` 进一步测试如下: ``` > a [ '9', '9', '9', '9', '9', '9', '9', '9', '9', '9' ] > b [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ] ``` 我们分析一个问题: * 一、在checker1()中,a[]的元素保存了相同的i值,是不是意味着所有的setTimeout()中a.push()操作其实是工作在一个环境中的?而相对的,由于在checker2()中,b[]保存了不同的i值,那么b.push()就得工作在不同的环境中(从而才能访问不同的i值)。是不是? * 二、所以,如果在checker2()中每一次迭代都在不同的环境中,是不是说每一次迭代都要消耗一个“创建一个环境”所需要的时间和空间?如果是这样,是不是就说明了`let i`其实效率远低于`var i`? OK. 最后说明一下,百度查不到东西是正常的,查到才不正常。^^.
10 - Geek_8d73e32020-06-08老师我发现运行以下代码会报错 for(let x = 0;x<1;x++){ var x = 100; } //Uncaught SyntaxError: Identifier 'x' has already been declared 在我理解中,let声明的x是在forEnv中,而我使用var声明的x因为javaScript早期设计,会在全局中声明一个x。这两个作用域是不会冲突的呀,为什么报错了?展开
作者回复: 我个问题真的把我考到了,很花了一些时间来分析它。 首先,简单地说,这个问题可以视为对如下两个语法的比较: ``` // 例1:如下成立 var x = 100; for (let x = 0; x < 1; x++) console.log(x); // 0 // 例2:如下不成立 for (let x = 0; x<1; x++) { var x = 100; } ``` 就是说,为什么上述“例2”是不成立的呢?从我们之前的分析来说,`var x`声明的变量`x`是位于外部(例如全局)的,因此与当前块中的`let x`应该是没有关系的。——这类似于“例1”。 先说答案:这是语法限制。 下面……重点来了:JavaScript在语法上不允许在同一个块中出现“声明与词法名字相同的标识符”。也就是说,在语法上: ``` // 例3:你既不能写 let x = 100; const x = 100; // 例4:(所以,)也不能写 let x = 100; var x = 100; ``` 只要是在同一个词法作用域中,与'let/const'相同的“标识符声明”就是不被许可的。 再次强调:这是语法声明上的限制,而与执行过程是无关的。 不过在ECMAScript的规范上,对这一点也是语焉不详的。——唯一与此相关的,就是在一个语法块中,会将所有的let/const名字登记在BoundNames表中,以完成“例3”所示的名字重复检查。这在如下章节: > https://tc39.es/ecma262/#sec-let-and-const-declarations-static-semantics-early-errors 但这个位置的语法查错(至少在ECMAScript中看起来)是与`var x`声明无关的。并且事实上在ECMAScript规范中,并没有对`var`语句定义任何的语法错误抛出。 然而“(在同一个块中)不能重复声明”的语法限制是真实存在的。这项限制存在于两个地方。 首先,语法parser引擎自己会处理这个重复检测(尽管ECMAScript没有定义)。parser过程会维护当前块的词法上下文,并且拒绝在forBody和forHead中出现这种重复声明。而且有趣的是,这个检测过程对于let/const,以及var来说是不同的。——具体来说,let/const是只检测当前词法作用域,而var是检测词法作用域栈(scopeStack, scope chains)。关于这一点的实现,可以在这里看到: > https://github.com/babel/babel/blob/master/packages/babel-parser/src/util/scope.js#L95 所以这是一个parser在语法解析中表现出来的结果。但带来了一个更有趣的示例: ``` // 示例5,不成立 for (let x in {}) { var x = 100 } // 示例6,成立 for (let x in {}) { let x = 100 } ``` 接下来,我们需要置疑:使用eval()来执行的话,会不会产生一个“提升到外部(例如global)的变量”呢? 答案是:也不会。而这也是唯一一处在ECMAScript中对这种现象做了解释的地方,原文是: > A direct eval will not hoist var declaration over a like-named lexical declaration. 也就是“直接的eval()是不能将对变量提升到同名的词法声明之外的”。也就是,如下代码会导致一个执行期的错误: ``` // 示例7,不成立 for (let x in {}) { eval('var x = 100') } ``` 而这一段的说明是被ECMAScript写进规范,并在执行期而`eval()`来处理的。参见这里: > https://tc39.es/ecma262/#sec-evaldeclarationinstantiation
共 5 条评论8 - Geek_8d73e32020-05-26老师,我发现,我运行这段代码的时候,并没有报错。 for(let i = 0;i<10;i++){ let i = 1000; console.log(i); }展开
作者回复: 这是因为 > `for (let i = 0...)` 和 > `{ let i = 1000; ...` 是在两个作用域里面。前者是forEnv,后者是bodyEnv。所以不冲突,不会算作重复声明。
7 - Wiggle Wiggle2019-11-22词法、词法作用域、语法元素……等等,这些概念特别模糊,老师有什么推荐的书吗?
作者回复: 《JavaScript语言精髓与编程实践》第三版。^^. 已经交稿,大概快要出了。 如果急用,可以看ECMAScript~ 别的书很少用语言层面来讲的。不过,另外,你可以看《程序原本》,对很多概念都是讲到的。在这里可以直接下载: https://github.com/aimingoo/my-ebooks
共 2 条评论7 - zcdll2019-11-20看不懂。。。第一个 switch 那个例子都看不懂。。
作者回复: case 'b' 永远执行不到,但它里面的x却已经声明了,并且导致case 'a'中的代码无法访问到外部的`x = 100`。 这说明case 'a'和case 'b'使用了同一个闭包。
共 5 条评论7 - Y2019-11-20既然是单语句就说明只有一句话,如果就一句话,还是词法声明,那就会创建一个块级作用域,但是因为是单语句,那一定就没有地方会用到这个声明了。那这个声明就是没有意义的。所以js为了避免这种没有意义的声明,就会直接报错。是这样嘛
作者回复: 不是,单语句也可以实现很复杂的逻辑的。如果单语句使用let/const声明,也一样可以包括逻辑。例如(这个当然不能执行): if (false) let x = 100, y = x++; // < 这里的x就被使用了
6 - 🐒🐱🐭🐮🐯...2020-05-20看了4遍 终于看懂了5
- Marvin2019-11-26如果使用let /const 声明for循环语句,会迭代创建作用域副本。那么不是和文中的: 对于支持“let/const”的 for 语句来说)“通常情况下”只支持一个块级作用域这句话相矛盾么?
作者回复: for (let/const ...) “通常情况下”只支持一个块级作用域。 for (let/const ... in/of ...) 会迭代创建作用域副本。 有一眯眯细微的不同哦。 ^^.
共 3 条评论5 - G2020-10-03老师您好,关于如何学习这门课,可否请您指点一下。 第一部分的内容我其实已经来回读了很多遍了,我接触js差不多一年,里面很多内容对我来说比较难懂。在读到后面内容的时候,我常常需要再翻回前面这些内容重新读,因为有一些前面章节我所没有理解的地方,在后续章节会讲到,这时候重新读我就会有新的收获。而且我在重新读的过程中,发现文章中的每一句话都是很有用的,少看一句话可能就会让我造成理解上的错误,每次重新读都有新收获这件事,让我开心又让我焦虑,因为这代表我并没有完全读懂任何一章。在继续学习第三部分的时候,我又发现我开始很难读懂文章,此时我不知道我应该先整体读完第三部分然后再回过头来重新读几遍,还是把每一篇文章尽量弄清楚,我目前采取的是后面这种方式,我已经学了差不多20天这个课程,但是目前也没有真正走出第二部分的内容,这个会不会是我这种学习方式不对。 希望周老师指点一下。展开
作者回复: 可以先“观其大略,不求甚解”,一遍两遍之后,再“务求精细,绝无遗漏”。读书也好,学习也好,不同的东西要用不同的方法来应对,这门课是适合反复研究的。 在这门课的结束语中说过:即便是同一个石狮子,在不同的层次看到的,仍然是不同的东西、不同的答案、不同的理解。所以,不要纠结于你之前的所得“是否错了”,你可能只是高度提高了,理解有了不同而已。——如果你能否定之前的所见,是提高;如果你有能力质疑它,也是提高;如果你能肯定它,还是提高。 不进则退,无论否定、质疑、肯定,皆是进步,但都不是终点。是谓学习。
4 - 海绵薇薇2019-11-23hello,老师好,一如既往有许多问题等待解答:) for(let/const ...) ... 这个语句有两个词法作用域,分别是 forEnv 和 loopEnv。还有一个概念是iterationEnv,这个是迭代时生成的loopEnv的副本。 对于forEnv和loopEnv的范围我不是很清楚,请老师指点。 for(let i = 0; i < 10; i++) setTimeout(() => console.log(i)) 1 如上代码,let i 声明的 i 在forEnv还是在loopEnv / iterationEnv里? 1.1 如果在loopEnv / iterationEnv里那么forEnv看起来就没啥用了 1.2 如果在forEnv(文章中说let只会执行一次,并且forEnv是lopEnv的上级),那么按理说console.log打印出来的都是11(参考于:晓小东) 2 关于单语 let a = 1 报错问题 2.1 如果是单语句中词法声明被重复有问题,那么with({}) let b = 1 这个报错就解释不通了。上面是说with有自己的块作用域,这个词法声明是在自己块语句中做的,并不会和别人冲突 2.2 同样的情况存在于for(let a...) ... 中,for也有自己的作用域,并且每次循环都会生成新的副本,也不应该存在重复问题 3 关于上面提到的eval eval('let a = 1'); console.log(a) // 报错 eval是不是自己也有一个作用域? 期待:)展开
作者回复: 1. 这个问题出在我对“for(let/const...)”这个语法没有展开讲,它跟“for(var...)”,以及后面的“for(let/const ... in/of)”其实都有区别。所以你套用它们的处理方法,结果都有点差异,对你结论会带来干扰。 你读一下ECMA这个部分: https://tc39.es/ecma262/#sec-for-statement-runtime-semantics-labelledevaluation 注意其中的第三节的具体说明: > IterationStatement: for(LexicalDeclarationExpression;Expression)Statement 在后续调用中,简单地说,就是这种情况下for语句会为每次循环创建 CreatePerIterationEnvironment()来产生一个新的IterationEnv。并且thisIterationEnv 与lastIterationEnv 之间会有关联。 2. with({}) let b = 1 这个语法报错,不是因为with()没有作用域,而是它的作用域称为“对象作用域”,而不是“词法作用域”。对象作用域只有在用作global的时候可以接受var和泄露的变量声明,其它情况下,它不能接受“向作用域添加名字”这样的行为——它的名字列表来自于属性名,例如obj.x对吧。 3. eval有一个自己的作用域。
4 - 晓小东2019-11-21老师您看下这段代码, 我在Chrome 打印有点不符合直觉, Second 最终打印的应该是2, 为什么还是1,2, 3; for (let i = 0; i < 3; i ++, setTimeout(() => console.log("Second" + i), 20)) console.log(i), setTimeout(() => console.log('Last:' + i), 30); 0, 1, 2 Second: 0, 1, 2 Last: 0, 1, 2展开
作者回复: 在node里很合理呀。 在node里的second值是:Second1,Second2,Second3 如果你把setTimeout()超时值都改成0,就看得到计算过程了。 0 1 2 Last:0 Second1 Last:1 Second2 Last:2 Second3
4 - 桔右2019-11-21假设允许的话,没有块语句创建的iterationEnv的子作用域,let声明就直接在iterationEnv作用域中,会每次循环重复声明。
作者回复: 是的。^^.
共 2 条评论4 - 二二2020-10-12你好老师,按照文章的解释,因为for循环中let会导致块级作用域,开销会变大,此处的开销可以粗略理解成时间。 var a = new Array(10000).fill(0) console.time('var') for(var i=0, len=a.length; i<len; i++){} console.timeEnd('var') console.time('let') for(let i=0, len=a.length; i<len; i++){} console.timeEnd('let') 在chrome devtool执行的结果,var会比let要慢许多,请问中间还发生了什么,导致var会比let慢呢?展开
作者回复: 这个是特难解释的,因为devtool测的也不见得是js引擎的结果,而且chrome自身也还对v8引擎有优化,不见得是ecmascript语言规范所表现出来的样子。 只以你的例子来说,由于var其实声明是在for语句之外一层的变量作用域(这里正好是全局作用域)中的,所以在for语言中访问var变量其实是要经过多一次的查找的。而let块是在for语句里,尽管每次都创建一个新的(从上一次复制而来),但是它们之间不需要嵌套,所以访问层次总是1。因此,大抵来说,是用空间(更多的作用域环境)换了时间(更少的访问层次)。 对于js引擎(包括jit优化引擎)来说,let以及它所对应的词法作用域是易于优化处理的,而var则很难,因为var中的名字是可增删的,因此不能缓存也不能做层次的消减。
2 - 青山入我怀2020-08-22老师,请问既然forEnv是loopEnv的上级,而iterationEnv又是loopEnv的副本,那么按道理在iterationEnv中对i的改动,在查找i时不都是会通过环境链回溯,找到forEnv这个运用域下的i吗,那么闭包现象发生时,找到的i应该是同一个i啊,感觉增加了副本无法避免这个问题啊?
作者回复: 你忘了通常来说的代码,类似于`for (let i =0; i<x; i++) ...`,会在下一次迭代之前先`i++`一次?如果是`let i in obj`运算,那么也会发生一次将属性名提取到i的操作~ 所以i是副本,但“通常”会被立即重写。当然,如果代码中没有`i++`这样类似重写的操作,那么这个复制副本的行为就浪费了。
2 - A. 成事在...2020-08-02老师我认为的是函数递归比for循坏开支大,首先函数递归会不断重复的在作用域链中生成global scope,如果递归10次就会有十个重复的global scope,for循环是块级作用域它确实也会重复的生成内部的上下文但是不会生成作用域链也就不会重复的去生成global scope,老师我理解的对吗
作者回复: 你是对的。在作用域链上,递归的链会更长,而for总是两个层级。但他们的生成的作用域的个数是相当的。但似乎这与“生成global scope”没什么关系,但我想这可能只是你在表述上的一点问题吧。
共 2 条评论2 - Geek_8d73e32020-06-08所以,语句for ( x ...) ...语法中的标识符x是一个词法名字,应该由for语句为它创建一个(块级的)词法作用域来管理之。 老师,对于这句话,如果我运行以下代码 var x = 1; let y = 2; 那么javaScript也会创建两个作用域?一个变量作用域管理x,一个词法作用域管理y? 那么如果全局中已经存在了变量作用域和词法作用域 为什么for(let i =0....)中, 这个i不在刚才的词法作用域中声明,而要重新再创讲一个词法作用域?展开
作者回复: 一般的“块级作用域”所在的环境中并没有“变量作用域”。所以块级作用域只能放let/const。 同时具有“变量作用域”和“词法作用域 ”的只有函数、全局和ES6之后的模块。所以,当在一个一般的“块级作用域”中声明了var的时候,它就必须被“提升”到上面三种环境中去存放。 所以下面的代码中: ``` for (...) { var x = 1; let y = 2; ... ``` 在这其中`y`是放在for的forBody块中的,而`x`是放在这个语句所在的、更外层的“变量作用域”中的。 这个变量作用域并不是为上面两行代码“专门创建”的。但是for(let i =0....)中的“词法作用域 ”是为for语句专门创建的,因此它跟更外层的(例如全局)并不是同一个。
2