JavaScript核心原理解析
周爱民
《JavaScript语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
立即订阅
3637 人已学习
课程目录
已更新 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核心原理解析
登录|注册

06 | x: break x; 搞懂如何在循环外使用break,方知语句执行真解

周爱民 2019-11-22
你好,我是周爱民。
上一讲的for语句为你揭开了 JavaScript 执行环境的一角。在执行系统的厚重面纱之下,到底还隐藏了哪些秘密呢?那些所谓的执行环境、上下文、闭包或块与块级作用域,到底有什么用,或者它们之间又是如何相互作用的呢?
接下来的几讲,我就将重点为你讲述这些方面的内容。

用中断(Break)代替跳转

在 Basic 语言还很流行的时代,许多语言的设计中都会让程序代码支持带地址的“语句”。例如,Basic 就为每行代码提供一个标号,你可以把它叫做“行号”,但它又不是绝对物理的行号,通常为了增减程序的方便,会使用“1,10,20……”等等这样的间隔。如果想在第 10 行后追加 1 行,就可以将它的行号命名为“11”。
行号是一种很有历史的程序逻辑控制技术,更早一些可以追溯到汇编语言,或可以手写机器代码的时代(确实存在这样的时代)。那时由于程序装入位置被标定成内存的指定位置,所以这个位置也通常就是个地址偏移量,可以用数字化或符号化的形式来表达。
所有这些“为代码语句标示一个位置”的做法,其根本目的都是为了实现“GOTO 跳转”,任何时候都可以通过“GOTO 标号”的语法来转移执行流程。
然而,这种黑科技在 20 世纪的 60~70 年代就已经普遍地被先辈们批判过了。这样的编程方式只会大大地降低程序的可维护性,其正确性或正确性验证都难以保障。所以,后面的故事想必你都知道了,半个多世纪之前开始的“结构化”运动一直影响至今,包括现在我与你讨论的这个 JavaScript,都是“结构化程序设计”思想的产物。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《JavaScript核心原理解析》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(13)

  • 不将就
    老师,问个问题,
    {
    let a=10
    }
    这是块级作用域

    {
    var a=10
    }
    在外层可以访问到a为未定义,这是不是可以说明{}这对括号里只有出现let/const才算有块级作用域?但是如下
    if(1){
    let b=10
    }
    这个if语句有括号而且用了let,老师为什么又说if语句没有块级作用域?

    作者回复: 不是。块级作用域与它内部声明了什么没关系。例如,一个“块语句{ }”就有一个块级作用域,哪怕它内部一行代码也没有。

    对于你说的if(1)这个例子来说,这里有两个语句,一个是if语句本身,它是个“单语句”(ECMAScript就是这么定义的,NodeJS的错误提示里也有),它没有块级作用域;而后面的一对大括号“{}”,是一个“块语句”,有一个块。

    传统习惯上过来的开发人员会把“if () { ... }”理解成一个语句,而在JavaScript中,这是两个语句。

    2019-11-22
    7
  • 海绵薇薇
    Hello,老师好:)阅读完文章还存在如下问题,期待有解答或方向,感谢:)

    try {

    ​ 1

    } finally {

    ​ console.log('finally')

    ​ 2

    }

    输出:

    > finally

    > 1

    1. try finally 语句输出的Result 是{type: normal, value: 1}。但是最后一个语句是finally中的2,value不应该是2吗?

    try {

    ​ throw 1

    } catch(ex) {

    ​ 2

    }

    这里确实输出了2。


    function foo() {

    ​ aaa: try {

    ​ return 1;

    ​ } finally {

    ​ break aaa;

    ​ }

    }

    return 1 Result是{type: return, value: 1}
    break Result是{type: break, value: empty, target: aaa}

    2. 这里finally中语句的结果却覆盖了try中语句的结果,这是一个特例吗?

    作者回复: 我之前没有注意过这个例子,倒是忽略了它在语句执行上的特点。不过这并不算特例。

    因为在finally{}块中的执行流程仍然会回到try{}块,例如说,你在try{}块中使用return语句,那么在return之前会执行到finally{}块,而finally{}执行完之后,还会回到try{}块里的return语句来返回。所以最终“完成并退出”整个try语句的,还是try块。

    在效果上,这类似于(也就是finally{}是一个call()):
    try {
      return void finally(), x;
    }
    catch {}

    2019-11-28
    4
  • zcdll
    返回 Empty 的语句,是不是还有 单独的一个 分号,和 if 不写大括号,或者大括号中为空?

    作者回复: 你说的都是。不过也不止的哟。比如说break语句自己就返回empty呀,还有continue,还有for语句的某些处理,以及yield等等,都有返回Empty的情况。

    2019-11-22
    4
  • 穿秋裤的男孩
    所谓“可中断语句”其实只有两种,包括全部的循环语句,以及 swtich 语句。

    老师,那forEach不属于循环语句吗?为什么break不可以在forEach中使用呢

    作者回复: 如果你说的是`for each ( ... in ...)`,那么这个语句不在ECMAScript的规范里面,在mozilla的spidermonkey引擎里,也是被废弃的特性了。

    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for_each...in

    2019-11-26
    2
    2
  • Real Aaron
    【学习方式大变化】

    前5讲看下来,主要有两个感觉:

    1、课程的内容非常深入而且重要,经常中间看到一段文字,就有一种“原来如此“的体验。
    2、逻辑顺序看不懂,看完一讲之后,好像学到一些零碎知识,但串不一起来。

    今天凑巧看到了加餐中的”学习这门课的正确姿势“,原来老师用心良苦,没有将知识点清晰的串起来是希望大家自己能主动理清思路,串出逻辑。

    参考加餐中的方法,今天换了一种学习方式:一边学习内容,一边将关键词和疑惑(dots)写在本子上,反复琢磨其中的来龙去脉。最终写满了两页纸,然后将其中的各个点串起来(connecting the dots),形成了下面的笔记。

    【本讲的一些记录和归纳】

    1、执行结果方面:

    JavaScript 是一门混合了函数式与命令式范型的语言,对函数和语句的不同处理,正是两种语言范型根本上的不同抽象模型带来的差异。

    本质上所有 JavaScript 的执行都是语句执行(包括函数执行),语句执行的过程因语句类型而异,但结果都返回的是一个“完成”结果。

    但【函数语句执行】和【普通语句(非函数)执行】的区别在于:函数语句执行返回的“完成”结果是值或者引用(未报异常的情况下),而普通语句执行返回的是一个完成状态(Completion)。


    2、执行过程方面:

    总体来讲,

    JavaScript 的执行机制包含两部分:【执行权(逻辑)】和【数据资源(数据)】

    JavaScript的执行(运行)环境:是一个后入先出的栈,栈顶就是当前“执行权”拥有者所持有的那一帧数据,运行环境通过函数的 CALL/RETURN 来模拟“数据帧”(也称上下文环境或作用域)在栈上的入栈和出栈过程。

    但"break labelName"这一语法跟上面不同,它表达一个位置的跳转,而不是一个数据帧的进出栈。

    另外,各种类型的语句执行过程(内部逻辑)也可能有差异:

    2.1 函数执行过程
    2.2 break 执行过程
    2.3 case 执行过程
    2.4 switch 执行过程
    2.5 循环语句执行过程
    2.6 try...catch 执行过程


    【仍旧未解的疑问】

    1、函数执行和语句执行返回的都是一个完成状态?还是函数执行返回的只能是值或引用?亦或是其他说法?

    希望老师能解答一下,非常感谢。

    作者回复: 谢谢Aaron。

    我想你读过前11章,看到第二篇加餐内容(“让JavaScript运行起来”)之后,你的大多数问题就都有解了,而且会对你已经领悟到的内容有许多“更新”,认识会再加深一些的。

    说回你最后的两个疑问。表达式执行(包括函数执行),本质上都是求值运算,所以它们应当只返回值。但是事实上所有的执行——包括函数、表达式和语句也都“同时”是可以返回完成状态,这样才能在表达式中向外抛异常,因为异常抛出就是一个完成状态。

    但是ECMAScript对所有在表达式层面上返回的“完成状态”做了处理,相当于在语言层面上“消化了”这些状态。所以绝大多数情况下,你认为表达式执行返回的Result是值或引用就好了。稍有例外的是,函数调用返回的是一个type为Return的完成状态,只不过它在内部方法Call处理之后,也已经变成了值而已。

    关于这个问题,正好是在这一课的留言中,我给Elmer的回复中解释了更多的细节。你可以看看。

    2019-12-12
    1
    1
  • Elmer
    觉得函数执行应该是语句执行的一部分或者一个特例,返回值都已经统一为文中的result。
    只不过函数执行具体实现了本身的上下文创建与回收,并用额外的栈来记录当前执行状况。
    两者都是流程控制的一种形式。关系应为语句执行包含函数执行。
    不知道理解的对不对。

    作者回复: 其实真实的情况与你想的有点区别(也与我在文章中讲的有点细节上的不同)。关键在于:所有的表达式,原则上都是既可以返回完成记录,也可以返回引用,也可以返回值的。

    后面两种比较容易理解,但表达式返回“完成记录”的意义在哪儿呢?多数情况下是没有意义的,但是只有允许这种情况,ECMAScript才能在表达式(的实现逻辑)中抛异常啊。所以多数情况下表达式返回值的Result都是值或引用两种,但偶尔也会返回类型为Throw的异常完成记录。也正是因为这个缘故,在ECMAScript中,所有所有的取表达式计算结果的写法,都采用类似下面这种模式:
    ```
    * Let ref be the result of evaluating ...
    * Let val be ? GetValue(ref)
    ```
    首先,在GetValue()里面会写,如果ref不是引用,那么就直接返回,这样GetValue就会把“异常类型”的完成记录原样抛出来。然后,你注意第二行中的那个“?”号,那个表明如果GetValue()的调用结果是“异常类型”的完成记录,那么就结束当前的执行,继续把异常往外抛。

    而且?号还有一个作用,就是直接从Normal类型的完成记录中把值解出来。也就是如果r是NormalCompletion,那么r = r.value。这样一来,就确保任何`?...`操作的结果,要么是异常被抛出,要么就是完成记录r中的值(r.value)。

    所以,事实上整个“表达式执行”的结果Result也是支持返回值为完成记录的(而不仅仅是引用和值),只是绝大多数都过滤掉了。

    接下来才是你的问题。函数执行也只是正常地返回了一个完成记录而已(如上面所说的,这是正常的行为,而不是语句执行的特例)。如果它是使用Return,那么也会在调用完成前被替换成Normal类型。然后函数调用操作会保证在完成之前得到的仅仅是一个一般的JavaScript语言类型中的数据(Result),或者非正常的完成类型。你看看这里就明白了:
    ```
    // FROM: https://tc39.es/ecma262/#sec-evaluatecall

    ...
    // 取函数调用结果
    * Let result be Call(func, thisValue, argList).

    // 断言:要么是非正常返回,要么就是语言类型
    * Assert: If result is not an abrupt completion, then Type(result) is an ECMAScript language type.
    ```

    而Call()是调用F.[[Call]]来实现的,它的主要代码就一行:
    ```
    // FROM: https://tc39.es/ecma262/#sec-call
    * Return ? F.[[Call]](V, argumentsList).
    ```
    注意这里的?号,就是要么抛异常出去,要么就是把结果(完成记录r)中的值(r.value)取出来了。——这里再强调一个小的关键点:函数调用是不能返回规范类型中的“引用”的,也就是说结果值已经用GetValue(ref)把值取出来过了。

    2019-12-10
    1
  • 晓小东
    老师有一个表达式执行让我感到困惑, 我在做位运算符的时候碰到这么一个现象
    let n = 2;
        let c1 = n != 0;
        let c2 = (n & (n - 1)) === 0;
        let c3 = n & (n - 1) === 0;
        console.log(c1, c2, c3);

    打印: true true 0
    c3 结果为什么变成了0 按照表达式 左右操作数的逻辑

    作者回复: 这个是优先级的问题。你在c3里面去掉了一对括号,运算符的优先级变了。

    2019-11-26
    1
    1
  • Summer
    1、可以理解为函数中return的设计是为了传递函数的状态,break的设计则是为了传递语句的状态么?
    2、可以认为break;只可以中断语句,不能用在函数中,break label;可以用在函数中,它返回了上一行语句的完成状态并作为所在函数的返回值?

    作者回复: 1. 可以。
    2. 不太对。break labelName只与“块”相关,与函数没直接关系。语句的“块”也是有返回值的,因为JavaScript里面存在“语句执行是有值的”这个设定。

    注意有许多语句是有“块(块级作用域)”的,而不仅仅是块语句(也就是一对大括号,它称为Block语句)。

    2019-11-23
    1
  • westfall
    第一次看到标签化语句,请问老师,标签化语句除了用来 break,在实际的开发中还有哪些应用场景?
    2019-11-22
    1
  • 阿鑫
    居然还有标签语句,涨见识了
    2019-12-02
  • Wiggle Wiggle
    我觉得函数执行与语句执行的区别就是:函数调用涉及到入栈出栈,语句执行不涉及。
    2019-11-25
  • leslee
    这篇看来要看很多次了……
    2019-11-22
  • shengsheng.net
    03年我在学vb的时候,并没有提到goto有学,不过后面越来越多资料指出goto问题。
    2019-11-22
收起评论
13
返回
顶部