JavaScript 核心原理解析
周爱民
《JavaScript 语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
32699 人已学习
新⼈⾸单¥59
登录后,你可以任选3讲全文学习
课程目录
已完结/共 28 讲
开篇词 (1讲)
JavaScript 核心原理解析
15
15
1.0x
00:00/00:00
登录|注册

05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销

for循环的代价
for循环的特殊处理
循环
分支
顺序
为什么单语句中不能出现词法声明
特例:for循环中的块级作用域
JavaScript语句的块级作用域
ECMAScript 6的块级作用域
语句的分类
语句的基本组成
块级作用域
JavaScript语句
JavaScript语句和块级作用域

该思维导图由 AI 生成,仅供参考

你好,我是周爱民。欢迎回到我的专栏,我将为你揭示 JavaScript 最为核心的那些实现细节。
语句,是 JavaScript 中组织代码的基础语法组件,包括函数声明等等在内的六种声明,其实都被归为“语句”的范畴。因此,如果将一份 JavaScript 代码中的所有语句抽离掉,那么大概就只会剩下为数不多的、在全局范围内执行的表达式了。
所以,理解“语句”在 JavaScript 中的语义是重中之重。
尽管如此,你实际上要了解的也无非是顺序分支循环这三种执行逻辑而已,相比于它们,其它语句在语义上的复杂性通常不值一提。而这三种逻辑中尤其复杂的就是循环,今天的这一讲,我就来给你讲讲它。

在 ECMAScript 6 之后,JavaScript 实现了块级作用域。因此,现在绝大多数语句都基于这一作用域的概念来实现。近乎想当然的,几乎所有开发者都认为一个 JavaScript 语句就有一个自己的块级作用域。这看起来很好理解,因为这样处理是典型的、显而易见的代码分块的结果
然而,事实上正好相反。
真正的状况是,绝大多数 JavaScript 语句都并没有自己的块级作用域。从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript中的循环语句在块级作用域方面有着特殊的设计,尤其是`for`循环语句。大多数JavaScript语句并没有自己的块级作用域,但`for`循环语句是一个特例,它具有自己的块级作用域。这种设计是出于标识符管理的需要,只有在语句中包含标识符声明时才需要创建块级作用域来管理这些标识符。文章还提到了`switch`语句和`try...catch...finally`语句等具有块级作用域的特例。循环语句中的块级作用域对性能有影响,强调了对JavaScript语句的理解和块级作用域的重要性。JavaScript在添加块级作用域特性时充分考虑了对旧有语法的兼容,因此当出现“var声明”时,它所声明的标识符与该语句的块级作用域无关。在ECMAScript中,这是两套标识符体系,也是使用两套作用域来管理的。文章深入讨论了循环语句中块级作用域的设计原则和实现细节,以及在具体执行过程中作用域被作为环境的上下文来创建的影响。文章内容深入浅出,通过讲解JavaScript语句的设计原则和实现细节,帮助读者更好地理解JavaScript中的循环语句及其块级作用域特点。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 核心原理解析》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(58)

  • 最新
  • 精选
  • Y
    老师,在es6中,其实for只要写大括号就代表着块级作用域。所以只要写大括号,不管用let 还是 var,一定是会创建相应循环数量的块级作用域的。 如果不用大括号,在for中使用了let,也会创建相应循环数量的块级作用域。 也就是说,可以提高性能的唯一情况只有(符合业务逻辑的情况下),循环体是单行语句就不使用大括号且for中使用var。

    作者回复: 是的。 赞,好几个赞。^^.

    2019-11-20
    13
    43
  • westfall
    因为单语句没有块级作用域,而词法声明是不可覆盖的,单语句后面的词法声明会存在潜在的冲突。

    作者回复: :) +1

    2019-11-20
    2
    21
  • 小毛
    老师,最后的思考题感觉有点懵,按你文章里说的,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 ...)循环语句中,要效率高一些。 如上。

    2020-03-05
    14
  • Elmer
    从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。 单语句如果支持变量声明,相当于需要支持为iteration env新增子作用域,降低了效率? 如果需要完全可以自己写{} 来强制生成一个子作用域 不知道这样说对不对

    作者回复: 正是如此😃👍

    2019-12-08
    12
  • wDaLian
    const 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. 最后说明一下,百度查不到东西是正常的,查到才不正常。^^.

    2020-01-12
    10
  • Geek_8d73e3
    老师我发现运行以下代码会报错 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

    2020-06-08
    5
    8
  • Geek_8d73e3
    老师,我发现,我运行这段代码的时候,并没有报错。 for(let i = 0;i<10;i++){ let i = 1000; console.log(i); }

    作者回复: 这是因为 > `for (let i = 0...)` 和 > `{ let i = 1000; ...` 是在两个作用域里面。前者是forEnv,后者是bodyEnv。所以不冲突,不会算作重复声明。

    2020-05-26
    7
  • Wiggle Wiggle
    词法、词法作用域、语法元素……等等,这些概念特别模糊,老师有什么推荐的书吗?

    作者回复: 《JavaScript语言精髓与编程实践》第三版。^^. 已经交稿,大概快要出了。 如果急用,可以看ECMAScript~ 别的书很少用语言层面来讲的。不过,另外,你可以看《程序原本》,对很多概念都是讲到的。在这里可以直接下载: https://github.com/aimingoo/my-ebooks

    2019-11-22
    2
    7
  • zcdll
    看不懂。。。第一个 switch 那个例子都看不懂。。

    作者回复: case 'b' 永远执行不到,但它里面的x却已经声明了,并且导致case 'a'中的代码无法访问到外部的`x = 100`。 这说明case 'a'和case 'b'使用了同一个闭包。

    2019-11-20
    5
    7
  • Y
    既然是单语句就说明只有一句话,如果就一句话,还是词法声明,那就会创建一个块级作用域,但是因为是单语句,那一定就没有地方会用到这个声明了。那这个声明就是没有意义的。所以js为了避免这种没有意义的声明,就会直接报错。是这样嘛

    作者回复: 不是,单语句也可以实现很复杂的逻辑的。如果单语句使用let/const声明,也一样可以包括逻辑。例如(这个当然不能执行): if (false) let x = 100, y = x++; // < 这里的x就被使用了

    2019-11-20
    6
收起评论
显示
设置
留言
58
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部