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

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

对比函数执行与语句执行的过程
其他返回Empty的语句
重要点总结
Empty值
break语句的返回值
完成状态
运行期语义
函数式与命令式范型
JavaScript的执行机制
作用域的管理
break语法
标签化语句
break语法
可中断语句
分块代码与流程控制技术
JavaScript中的GOTO语句
结构化程序设计
历史背景
块与块级作用域
闭包
上下文
执行环境
思考题
知识回顾
中断语句的特殊性
语句执行的意义
执行现场的回收
第二种中断
第一种中断
用中断(Break)代替跳转
JavaScript执行环境
JavaScript语句执行的真解就在循环外使用break

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

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

用中断(Break)代替跳转

在 Basic 语言还很流行的时代,许多语言的设计中都会让程序代码支持带地址的“语句”。例如,Basic 就为每行代码提供一个标号,你可以把它叫做“行号”,但它又不是绝对物理的行号,通常为了增减程序的方便,会使用“1,10,20…...”等等这样的间隔。如果想在第 10 行后追加 1 行,就可以将它的行号命名为“11”。
行号是一种很有历史的程序逻辑控制技术,更早一些可以追溯到汇编语言,或可以手写机器代码的时代(确实存在这样的时代)。那时由于程序装入位置被标定成内存的指定位置,所以这个位置也通常就是个地址偏移量,可以用数字化或符号化的形式来表达。
所有这些“为代码语句标示一个位置”的做法,其根本目的都是为了实现“GOTO 跳转”,任何时候都可以通过“GOTO 标号”的语法来转移执行流程。
然而,这种黑科技在 20 世纪的 60~70 年代就已经普遍地被先辈们批判过了。这样的编程方式只会大大地降低程序的可维护性,其正确性或正确性验证都难以保障。所以,后面的故事想必你都知道了,半个多世纪之前开始的“结构化”运动一直影响至今,包括现在我与你讨论的这个 JavaScript,都是“结构化程序设计”思想的产物。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript语句执行的真解就在循环外使用break。本文深入探讨了JavaScript中的中断机制,包括使用break中断可中断语句和标签化语句的两种方式。文章强调了JavaScript执行现场的回收机制,以及break语句对执行过程的影响和资源回收的重要性。此外,还探讨了语句执行的返回值、中断语句的特殊性以及最小化的break语句示例。通过对函数执行与语句执行的对比,展现了JavaScript执行环境的独特特点。文章还回顾了GOTO语句的有害性以及新语法的设计目的。整体而言,本文通过深入解析JavaScript中的中断机制,展现了其独特的执行方式和与传统块级作用域的区别,为读者提供了对JavaScript执行环境的深入理解。

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

全部留言(27)

  • 最新
  • 精选
  • 不将就
    老师,问个问题, { 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
    30
  • 海绵薇薇
    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
    20
  • 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
    3
    10
  • zcdll
    返回 Empty 的语句,是不是还有 单独的一个 分号,和 if 不写大括号,或者大括号中为空?

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

    2019-11-22
    9
  • 林逸舟
    尝试完整地对比函数执行与语句执行的过程: ·操作返回值: 函数执行:在函数体的最后进行一次返回值的赋值 语句执行:在每句后更新返回值 如下所示: function foo() { 1 + 1; return 1; //函数的执行结果为1 赋值动作仅有一次 } foo(); { 1 + 1; //整个块语句的执行结果更新为2 2 + 2; //整个块语句的执行结果更新为4 } ·堆栈顺序 函数执行与语句执行类似 是先入后出的堆栈 如下所示: function bar() { return; } function foo() { 1 + 1; bar() return 1; //函数的执行结果为1 赋值动作仅有一次 } foo(); //开始执行foo->开始执行bar->bar执行结束->foo执行结束 { 1 + 1; //整个块语句的执行结果更新为2 2 + 2; //整个块语句的执行结果更新为4 } //开始执行块语句{}->执行1+1->1+1执行结束->执行2+2->2+2执行结束->块语句执行结束 ·操作Result的对象 函数:getValue(ref)||ref传递给上一层表达式使用 语句:Completion传递给引擎进行使用

    作者回复: 谢谢。挺好的一份答案。 其中有些东西阅读到后面的小结就可以得到印证了。^^.

    2020-04-06
    2
    6
  • 黑山老妖
    1、传统习惯上过来的开发人员会把“if () { ... }”理解成一个语句,而在JavaScript中,这是两个语句。 2、在try{}块中使用return语句,那么在return之前会执行到finally{}块,而finally{}执行完之后,还会回到try{}块里的return语句来返回。所以最终“完成并退出”整个try语句的,还是try块。 3、·操作Result的对象 函数:getValue(ref)||ref传递给上一层表达式使用 语句:Completion传递给引擎进行使用 4、所谓“可中断语句”其实只有两种,包括全部的循环语句,以及 swtich 语句。 5、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、函数执行和语句执行返回的都是一个完成状态?还是函数执行返回的只能是值或引用?亦或是其他说法?表达式执行(包括函数执行),本质上都是求值运算,所以它们应当只返回值。但是事实上所有的执行——包括函数、表达式和语句也都“同时”是可以返回完成状态,这样才能在表达式中向外抛异常,因为异常抛出就是一个完成状态。 但是ECMAScript对所有在表达式层面上返回的“完成状态”做了处理,相当于在语言层面上“消化了”这些状态。所以绝大多数情况下,你认为表达式执行返回的Result是值或引用就好了。稍有例外的是,函数调用返回的是一个type为Return的完成状态,只不过它在内部方法Call处理之后,也已经变成了值而已。 1、可以理解为函数中return的设计是为了传递函数的状态,break的设计则是为了传递语句的状态么?可以 2、可以认为break;只可以中断语句,不能用在函数中,break label;可以用在函数中,它返回了上一行语句的完成状态并作为所在函数的返回值?. 不太对。break labelName只与“块”相关,与函数没直接关系。语句的“块”也是有返回值的,因为JavaScript里面存在“语句执行是有值的”这个设定。 注意有许多语句是有“块(块级作用域)”的,而不仅仅是块语句(也就是一对大括号,它称为Block语句)。 函数执行啊,其实是表达式执行的特例。它会通过完成记录来返回return语句返回结果。 但是,在内部过程Call()的调用中它会取出值,而不是直接返回“Return类型的完成类型”。所以在“函数调用作为表达式的操作数”时,运算处理的还是“Result/Value值”,而不是“完成记录”。 由于函数调用会“从完成记录中取出值”,所以它不能返回“引用(规范类型)” 在js中,语句执行跟表达式执行是分开的,是两种不同概念的东西。而函数执行其实是表达式执行的一种,其中函数名(亦即是函数)是运算数,而一对括号是运算符。——这是确实的,并且这个称为“函数调用运算符”的括号也是有优先级的,你可以直接在MDN里面查到。 表面来看,函数就是一堆语句,但其实“函数执行”时的返回值是由return来决定的,对吧。而语句执行却不是,语句执行的结果值是由“最后一个有效语句”来决定的。当你使用eval()来执行一批语句时,就可以看到这个结果值了。——并且,这也是语句执行要被拿出来讨论的原因,亦即是“动态执行”执行的是语句,而不是函数,也不是表达式。

    作者回复: 非常棒的总结! 关于未解疑问中的第1个,你自己的回答是对的,就是语言层面上消化了那些问题。——既然RETURN是一个完成状态,那么就一定是按语句返回来做的,但f()是一个“函数调用表达式”,那就是最终按表达式运算结果做了处理。 两个分开的小的疑问点。其一,函数的RETURN不是用来“传状态”。如果函数内有一个特别的状态,那一定是THROW,如果没这样的状态,那就一定RETURN出来个东西。函数的RETURN要与参数结合起来理解,就是一进一出的求值=>数据传递方式。——多说一句,yield带来的是多进多出的数据传递。 其二是关于break lableName的,你的理解没错。它只与块相关。 其它的都非常赞,表达的也清晰明了。再赞+n

    2022-01-13
    5
  • 穿秋裤的男孩
    所谓“可中断语句”其实只有两种,包括全部的循环语句,以及 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
    6
    5
  • 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
    2
    4
  • 桔右
    1、可以理解为函数中return的设计是为了传递函数的状态,break的设计则是为了传递语句的状态么? 2、可以认为break;只可以中断语句,不能用在函数中,break label;可以用在函数中,它返回了上一行语句的完成状态并作为所在函数的返回值?

    作者回复: 1. 可以。 2. 不太对。break labelName只与“块”相关,与函数没直接关系。语句的“块”也是有返回值的,因为JavaScript里面存在“语句执行是有值的”这个设定。 注意有许多语句是有“块(块级作用域)”的,而不仅仅是块语句(也就是一对大括号,它称为Block语句)。

    2019-11-23
    4
  • Elmer
    求函数执行与语句执行的过程对比。

    作者回复: 函数执行啊,其实是表达式执行的特例。它会通过完成记录来返回return语句返回结果。 但是,在内部过程Call()的调用中它会取出值,而不是直接返回“Return类型的完成类型”。所以在“函数调用作为表达式的操作数”时,运算处理的还是“Result/Value值”,而不是“完成记录”。 由于函数调用会“从完成记录中取出值”,所以它不能返回“引用(规范类型)”。举例来说: ``` # 示例 > obj = { foo() { return this === obj } } # 分组表达式能返回引用 > (obj.foo)() true # return不能返回引用 > (function () { return obj.foo })()() false ```

    2019-12-26
    3
收起评论
显示
设置
留言
27
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部