06 | x: break x; 搞懂如何在循环外使用break,方知语句执行真解
该思维导图由 AI 生成,仅供参考
用中断(Break)代替跳转
- 深入了解
- 翻译
- 解释
- 总结
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-2230 - 海绵薇薇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-2820 - 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-10310 - zcdll返回 Empty 的语句,是不是还有 单独的一个 分号,和 if 不写大括号,或者大括号中为空?
作者回复: 你说的都是。不过也不止的哟。比如说break语句自己就返回empty呀,还有continue,还有for语句的某些处理,以及yield等等,都有返回Empty的情况。
2019-11-229 - 林逸舟尝试完整地对比函数执行与语句执行的过程: ·操作返回值: 函数执行:在函数体的最后进行一次返回值的赋值 语句执行:在每句后更新返回值 如下所示: 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-0626 - 黑山老妖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-135 - 穿秋裤的男孩所谓“可中断语句”其实只有两种,包括全部的循环语句,以及 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-2665 - 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-1224 - 桔右1、可以理解为函数中return的设计是为了传递函数的状态,break的设计则是为了传递语句的状态么? 2、可以认为break;只可以中断语句,不能用在函数中,break label;可以用在函数中,它返回了上一行语句的完成状态并作为所在函数的返回值?
作者回复: 1. 可以。 2. 不太对。break labelName只与“块”相关,与函数没直接关系。语句的“块”也是有返回值的,因为JavaScript里面存在“语句执行是有值的”这个设定。 注意有许多语句是有“块(块级作用域)”的,而不仅仅是块语句(也就是一对大括号,它称为Block语句)。
2019-11-234 - Elmer求函数执行与语句执行的过程对比。
作者回复: 函数执行啊,其实是表达式执行的特例。它会通过完成记录来返回return语句返回结果。 但是,在内部过程Call()的调用中它会取出值,而不是直接返回“Return类型的完成类型”。所以在“函数调用作为表达式的操作数”时,运算处理的还是“Result/Value值”,而不是“完成记录”。 由于函数调用会“从完成记录中取出值”,所以它不能返回“引用(规范类型)”。举例来说: ``` # 示例 > obj = { foo() { return this === obj } } # 分组表达式能返回引用 > (obj.foo)() true # return不能返回引用 > (function () { return obj.foo })()() false ```
2019-12-263