JavaScript核心原理解析
周爱民
《JavaScript语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
立即订阅
3607 人已学习
课程目录
已更新 21 讲 / 共 21 讲
0/3登录后,你可以任选3讲全文学习。
开篇词 (1讲)
开篇词 | 如何解决语言问题?
免费
从零开始:JavaScript语言是如何构建起来的 (5讲)
01 | delete 0:JavaScript中到底有什么是可以销毁的
02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题
04 | export default function() {}:你无法导出一个匿名函数表达式
05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销
从表达式到执行引擎:JavaScript是如何运行的 (6讲)
06 | x: break x; 搞懂如何在循环外使用break,方知语句执行真解
07 | `${1}`:详解JavaScript中特殊的可执行结构
08 | x => x:函数式语言的核心抽象:函数与表达式的同一性
09 | (...x):不是表达式、语句、函数,但它却能执行
10 | x = yield x:迭代过程的“函数式化”
11 | throw 1;:它在“最简单语法榜”上排名第三
从原型到类:JavaScript是如何一步步走向应用编程语言的 (6讲)
12 | 1 in 1..constructor:这行代码的结果值,既可能是true,也可能是false
13 | new X:从构造器到类,为你揭密对象构造的全程
14 | super.xxx():虽然直到ES10还是个半吊子实现,却也值得一讲
15 | return Object.create(new.target.prototype):做框架设计的基本功:写一个根类
16 | [a, b] = {a, b}:让你从一行代码看到对象的本质
17 | Object.setPrototypeOf(x, null):连Brendan Eich都认错,但null值还活着
不定期加餐 (3讲)
加餐 | 捡豆吃豆的学问(上):这门课讲的是什么?
免费
加餐 | 捡豆吃豆的学问(下):这门课该怎么学?
免费
加餐 | 让JavaScript运行起来
免费
JavaScript核心原理解析
登录|注册

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

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

在 ECMAScript 6 之后,JavaScript 实现了块级作用域。因此,现在绝大多数语句都基于这一作用域的概念来实现。近乎想当然的,几乎所有开发者都认为一个 JavaScript 语句就有一个自己的块级作用域。这看起来很好理解,因为这样处理是典型的、显而易见的代码分块的结果
然而,事实上正好相反。
真正的状况是,绝大多数 JavaScript 语句都并没有自己的块级作用域。从语言设计的原则上来看,越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《JavaScript核心原理解析》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(20)

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

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

    2019-11-20
    3
    13
  • zcdll
    看不懂。。。第一个 switch 那个例子都看不懂。。

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

    这说明case 'a'和case 'b'使用了同一个闭包。

    2019-11-20
    2
    4
  • Marvin
    如果使用let /const 声明for循环语句,会迭代创建作用域副本。那么不是和文中的:
    对于支持“let/const”的 for 语句来说)“通常情况下”只支持一个块级作用域这句话相矛盾么?

    作者回复: for (let/const ...) “通常情况下”只支持一个块级作用域。
    for (let/const ... in/of ...) 会迭代创建作用域副本。

    有一眯眯细微的不同哦。 ^^.

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

    作者回复: 《JavaScript语言精髓与编程实践》第三版。^^.
    已经交稿,大概快要出了。

    如果急用,可以看ECMAScript~ 别的书很少用语言层面来讲的。不过,另外,你可以看《程序原本》,对很多概念都是讲到的。在这里可以直接下载:
    https://github.com/aimingoo/my-ebooks

    2019-11-22
    3
  • Summer
    假设允许的话,没有块语句创建的iterationEnv的子作用域,let声明就直接在iterationEnv作用域中,会每次循环重复声明。

    作者回复: 是的。^^.

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

    作者回复: :)
    +1

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

    作者回复: 不是,单语句也可以实现很复杂的逻辑的。如果单语句使用let/const声明,也一样可以包括逻辑。例如(这个当然不能执行):

    if (false) let x = 100, y = x++; // < 这里的x就被使用了

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

    作者回复: 正是如此😃👍

    2019-12-08
    2
  • 海绵薇薇
    hello,老师好,一如既往有许多问题等待解答:)

    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有一个自己的作用域。

    2019-11-23
    2
  • 晓小东

    老师您看下这段代码, 我在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

    2019-11-21
    2
  • Fans
    看到标题感觉的终于有一个可能会看的懂了
    2019-11-21
    1
    1
  • 许童童
    为什么单语句(single-statement)中不能出现词法声明( lexical declaration )?
    我觉得应该是语法规定 单语句后面需要一个表达式,而一个声明语句是不行的。

    作者回复: 你可以在后面用var声明试试😊

    2019-11-20
    1
  • qqq
    单语句对应的是变量作用域,不能出现词法声明吗

    作者回复: 不是。

    而是这种情况下并没有所谓的“块级作用域(变量作用域)”。

    这就是需要仔细地“数”清楚一个语法有几个作用域的原因。

    2019-11-20
    1
  • Chao
    临时死区使得 通过let / const 定义的变量。 在定义之前调用报错。

    作者回复: 临时死区 = ?

    2019-11-20
    1
  • leslee
    老师好, 跟随老师学到了第10讲了, 但是回头看了一下, 发现我并没有真正的写到东西, 在看了老师的加餐后, 醒悟了, 并没有追求进度, 所以回过头来重新看第一章, 每一讲都看了不下三遍, 算是学到了一些知识, 在这一章的最后一讲, 还有些疑问, 望老师解答. 感谢

    `for(let inForEnv i in {}) let inLoopEnv;`

    1. 上文说在迭代的过程中会为后面的 let inLoopEnv 循环体创建相应数量的作用域, 但是又说使用了 let 的for单语句 通常只有一个块级作用域, 我理解的是 '创建相应数量的作用域' 是动态的,是运行时的 跟词法作用域无关, 而 '只有一个块级作用域' 是词法作用域, 也就是说 let inForEnv 跟 let inLoopEnv 在同一个词法作用域了, 每次迭代let都会冲突, 所以不允许在单语句中使用let词法声明,`Uncaught SyntaxError: Lexical declaration cannot appear in a single-statement context` 所以这个错其实是静态解析的错是么

    2: forEnv 是 loopEnv 的外部环境, 那么 IterationEnv 的外部环境是否是loopEnv , 上面所说 `创建了第二个作用域的无数个副本` 那这几个环境的层级是怎样的, 是 forEnv -> loopEnv -> iterationEnv 还是 forEnv-> loopEnv/iterationEnv

    作者回复: 1. 你的结论是对的,这里的确是一个静态解析异常。另外,“词法作用域”如果理解为静态的,那么for语句只有一个;如果考虑它在执行期的效果,那么for语句中带有let/const声明时,它会有“迭代次数+1”个。这就好象函数递归调用,函数实例只有一个,但闭包其实是递归次数个一样。它们的原因、性质和效果都是类似的。

    2. 对于“for (let/const...”来说,这个语句在处理的时候,所有的IterationEnv的parent指向相同的、外部的环境,也就是指向loopEnv。这个代码在这里:
    https://tc39.es/ecma262/#sec-createperiterationenvironment
    它是每次迭代都创建一个新的IterationEnv,并根据perIterationBindings[]来抄写上一次迭代环境中的值到新环境。

    而对于“for (let/const ... in/of)” 这个语法来说,在实现的逻辑上稍有区别,在这里:
    https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset

    但是在你的问题上答案是一致的,每次迭代都是iterationEnv的parent指向loopEnv。因为在上面的代码中,迭代是基于迭代器的循环,而每次创建新的iterationEnv时使用的将是相同的outer/parent值,也就是代码中的oldEnv,也就是loopEnv。

    2019-12-17
  • 杰妮蛇精病
    老师,switch case 如果加了{} 就会转成块级作用域了吧。
    正在用AST写工具,测switch的时候并没有意识到case的作用域问题,现在感觉最好把ecma读一遍。

    作者回复: 那样的话,也是{}这个块语句自己的块级作用域啊。{}是一个语句呢,放在switch语句之内,就变成好子级的语法树了。而`if () {}`等等也是如此,if自己没有块级作用域,而`{}`是它子级的语法树。

    2019-12-13
  • Jing
    考虑到对传统 JavaScript 的兼容,函数内部的顶层函数名是提升到变量作用域中来管理的。其中顶层函数名的指的是那些isNaN(),Number()这些JavaScript内置顶层函数吗

    作者回复: 不是。

    这里的“顶层”是作用域的说法,就是作用域的最外层。比如在function f() {}里面,如果有多级嵌套的函数,那么就是最外层的那一个(但还是f()的内的嵌套函数声明)。

    2019-12-13
  • 一路向北
    凭我的一点C语言基础很难理解JS了😇

    作者回复: 其实入门和应用的话,看JS的手册或语法书就可以了,这门课程主要的目标不是在“讲js怎么用”的。简单的话,我还是挺推荐w3cschool的,包括这里:
    https://www.w3school.com.cn/js/index.asp
    或这里:
    https://www.w3cschool.cn/javascript/

    2019-12-07
  • 小童
    if也没有块级作用域吗?

    作者回复: 没有的。

    如果你写成`if () { ... }`,那么那个块级作用域是后面那个块语句的,不是if语句的。比如,条件表达式`()`就不会在那个块里面执行。

    2019-12-07
    1
  • 南墙的树
    晓小东的一波操作,我还没见过这种写法,学习了
    2019-12-03
收起评论
20
返回
顶部