CategoryResourceRepost/极客时间专栏/JavaScript核心原理解析/从表达式到执行引擎:JavaScript是如何运行的/加餐 | 让JavaScript运行起来.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

239 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="加餐 | 让JavaScript运行起来" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/db/4b/dbc09a86c81e6dfc4daddbe191e1954b.mp3"></audio>
你好我是周爱民。欢迎回到我的专栏。今天是传说中的加餐时间我将与你解说前11讲内容的整体体系和结论。
我们从一个问题讲起那就是JavaScript到底是怎么运行起来的呢
看起来这个问题最简单的答案是“解析→运行”。然而对于一门语言来说“引擎解释与运行”都是最终结果的表象真正处于原点的问题其实是“JavaScript运行的是什么
在前11讲中我是试图将JavaScript整个的运行机制摊开在你的面前因此我们有两条线索可以抓
1. 表面上,它是讲引用和执行过程;
1. 在底下讲的是引擎对“JavaScript是什么”的理解。
## 从文本到脚本
我们先从第二条线索,也就是更基础层面的线索讲起。
JavaScript的所谓“脚本代码”在引擎层面看来首先就是一段文本。在性质上装载`a.js`执行与`eval('...')`执行并没有区别,它们的执行对象都被理解为一个“字符串”,也就是字符串这一概念本身所表示的、所谓的“字符序列”。
在字符序列这个层面上,最简单和最经济的处理逻辑是**正向遍历**,这也是为什么“语句解析器”的开发者总是希望“语言的设计者”能让他们“一次性地、不需要回归地”解析代码的原因。
回归也就是查看之前“被parser过的代码”就意味着解析器需要暂存旧数据无法将解析器做得足够简洁进而无法将解析器放在小存储的环境中。根本上来说JavaScript解析引擎是“逐字符”地处理代码文本的。
JavaScript从“逐字符处理”得到的引擎可以理解的对象称为记号Tokens。这个概念是从第一讲就开始提的你回顾第一讲的内容在提出`Tokens`这个概念的时候,有这样一句话:
>
一个记号是没有语义的,记号既可以是语言能识别的,也可以是语言不能识别的。唯有把这二者同时纳入语言范畴,那么这个语言才能识别所谓的“语法错误”。
我之所以用**“delete运算”**作为《JavaScript核心原理解析》的开篇是因为在我看来这讲的是一种“不知死即不知生”的道理。如果你不知道一个东西是如何被毁灭的那么你也不知道它创生的意义。
然而,这个理解也可以倒过来,是所谓的“不知生,亦不知死”。也就是说,如果你都不知道它被创造出来的时候是什么,那么你也不知道你毁灭了什么。
而这个记号Tokens就是引擎从文本到脚本JavaScript引擎也好、语言也好它们创造出来的第一个东西——也是在创世原点唯一的东西。
记号,要么是可识别的,要么是不能识别的。并且,它们必须同时纳入语言范畴。这个“必须同时纳入”,决定了二者不是相互孤立的元素,而是同一体系下的东西,也就是所谓的“体系的完整性”。
## 引用与静态语言的处理
看完底层的线索我们再来看看JavaScript运行机制的表面线索。
引用References是静态语言与引擎之间的桥梁它是ECMAScript规范中最大的一个挑战你理解了“规范层面的引用References也就基本上理解了ECMAScript规范整个的叙述框架。这个框架的核心在于——ECMAScript的目的是描述“引擎如何实现”而不是“描述语言是什么”。
规范层面中的引用与引擎的核心设计有关。
在JavaScript语言层面它希望引擎是一个执行器更具体的描述是引擎的核心是一个表达式计算的、连续的执行过程。表达式计算是整个JavaScript语言中最核心的预设一旦超出这个预设JavaScript语言的结构体系就崩溃了。
所以本质上来说JavaScript的所谓“语句能执行”也是一个或一组表达式计算过程而且所有的计算都必须能描述成一个基本的模式`opCode -&gt; opData`,也就是用操作符去处理操作数。
这个相信你也明白了,这回到了我们计算理论最初的原点,是我们学习计算机这门课程最初的那个设定:**计算实现的就是”计算求解“的过程**。它的另一个公式化的表达就是著名的”算法+数据结构=程序“。
当然,这个说得有点远了,在这个概念集合中,最关键的点在于“**执行过程最终是表达式计算**”。因此,语句执行也是表达式计算,函数调用也是表达式计算,各种特殊执行结果还是表达式计算。
这些“计算”总会有一个返回值,是什么呢?
你可以参考文章里的这张图它说明了JavaScript中最核心的两种执行过程它们都被称为evaluating是如何最终被统一的。
<img src="https://static001.geekbang.org/resource/image/b2/11/b281ed38f4894823bdb0daf771d27311.jpg" alt="">
在语句执行的层面它返回一个语句的完成状态这个状态中包括了一个“value”域它必须且必然会是JavaScript语言理解的类型也就是typeof()所识别的所有的值。这样一来任何“语句”“代码”或“代码文本”就都可以被执行了并且都可以使用console.log()输出结果给你了。
这其中最重要的一件事是在任何语句执行并得到结果时如果它“当时”是一个所谓的“引用”那么这个引用就必须先调用“GetValue(x)”来得到值然后放到这个“value”域中去。因为“引用”是一个规范层面的东西它不是JavaScript语言能理解的也无法展示给开发者。
最后ECMAScript约定可以在“value”域中放上`Empty`,这表明语句执行“没有值”。它能表明有值,也能表明无值,仍然是“概念完整性”。
而到了表达式执行时(注意函数调用也是表达式执行的一种),这个过程又被重来了一回。不过表达式执行会返回两个东西:它要么直接返回一个“上面的完成结果所理解的值”,要么返回一个包含这样的值的“引用”。
你可能会说了,不对呀——你刚才还说所谓“概念的完整性”,是“要么返回东西,要么返回没有东西”啊。
对的在表达式执行这个体系里面“没有东西”是所谓的“不可发现的引用UnresolvableReference”。
所以完整的概念集是value、引用Reference和不可发现的引用UnresolvableReference
一个不可发现的引用是能被处理的,例如`delete x`,或者`typeof x`。所有“能处理引用的”运算符都能处理它。当然在严格模式中会在语法分析阶段就报异常那是另一个层面的东西有机会的时候我们再聊。这里在JavaScript语言层面它仍然在维护一种简单的完整性。
那么,为什么要有“引用”这么个东西呢?
你想想如果没有引用你就得将所有的东西都直接当成一个被处理的对象例如用1G的内存来处理一个1G文本的记号。这显然不可行。我们可以用一个简单的法子来解决就是加一个指针指向它在不需要访问它的“内容”时我们就访问这个指针好了。而引用也就是所有在“不访问内容”的情况下用于指向这个内容的一个结构。它叫什么名字其实都好、都行重点的是
1. 它代表这个东西r(x)。
1. 它包含这个东西所以可以x = GetValue®。
所以本质上,引用还是指向值、代表值的一个概念,它只是“获得值的访问能力”的一个途径。最终的结果仍然指向原点:计算值、求值。
## 结构与体系的回顾
讲完JavaScript整个运行机制的两条线索后就是加餐的最后一部分内容了我会直接为你解说前11讲的主题。
### 模块一体系1
- 1 | delete 0
讲述的是“规范引用”将“规范引用”与传统概念中的引用区别开来。用Result来指代执行结果的“引用状态和值状态未区分”。同时指明“状态未区分”的原因是同一个标识符在作为_lhs_和_rhs_的时候意义是不同的并且在计算没有“推进到”下一步之前上一步的Result是无法确知“将作为”**lhs**/_rhs_的哪一种操作数的。
JavaScript确实有一部分表达式或操作是能处理“规范引用”的例如`delete x`就是其中之一。有关哪些运算能处理“规范引用”建议你自己翻阅ECMAScript并从中归纳。
- 2 | var x = y = 100
这一讲的核心是讲六种声明。所有声明语句都是没有返回值的返回Empty因为它没有返回值所以它对其他执行过程没有影响。也就是说声明语句必须能被理解为“静态分析的结果”而不是“动态执行的结果”。
前者称为“声明语义”,后者称为“执行语义”。声明语义就是静态语言的处理,执行语义就是动态语言的处理。这是两种语言范型的分水岭。
- 3 | a.x = a = {n:2}
这一讲的核心是讲表达式执行与(看起来跟它相似的)语句声明之间的区别。虽然两种看起来都相似,但其实只有这一讲的才是“表达式连等”。
在这一讲结尾的部分,我做了一个总结:有关“引用”的介绍,以及“语句”和“表达式”之间的差异与分别,自此暂告段落。
- 4 | export default function() {}
这一讲的核心是讲“名字”的使用。“有名字/没有名字”是一对概念而“没有名字”就称为“default”那就是将概念收敛到了唯一一个名字。所有有关export/import的处理就是名字与它所代表的东西之间的关系映射。
而“模块装载的过程”必须发生在用户代码之前,一共包括了两个意思:
1. 引擎必须有一个依赖顺序来“初始化”那些名字这个与export语句是“声明”有关声明意味着它是静态完成的名字总是被静态声明的
1. 用户代码需要依赖那些名字这与import语句不是“声明”有关它不是声明那么它需要通过“执行”来得到结果的而这些“执行”必须在用户代码之前。其顺序就是所谓模块装载树的遍历。
- 5 | for (let x of [1,2,3]) …
这一讲的要点不是讲语句执行,而是讲**块级作用域**,更进一步的,它是在讲作用域的“识别”与处理。它颠覆读者认知的地方在于提出:绝大多数语句并没有块级作用域,因为**它们不需要**。
而需要块级作用域的for语句根本的需求是需要处理多次迭代中的变量暂存。这个是有很大开销的这与“计算机语言”的一个核心原理有关迭代需要循环控制变量这是命令式语言有变量的根源之一也是函数式语言需要处理递归的根源。
“需不需要存储计算过程中的变量”,也是命令式语言与函数式语言的分水岭。
以上是前5讲的内容。 到现在为止,在第一模块中,我们主要提出的是语言的三个层面的概念:
- 第一层概念:记号
- 第二层概念:引用、值
- 第三层概念:表达式、语句、名字、环境/作用域 、(顺序执行的三种基础)逻辑
>
NOTE: 这主要是在[《程序原本》](https://github.com/aimingoo/my-ebooks)前三章中的概念,包括“数、逻辑和抽象”。部分涉及到第四章,也就是“语言”中的概念。
这些概念其实基本上都是在“代码的静态组织”过程中就完成/实现了的。你使用一门语言其实本质上就是在跟第三层概念打交道而ECMAScript或者引擎是工作在第二个层面的。第一个层面则是物理层面与逻辑层面的、最初的映射。
### 模块二体系2
接下来我们讨论第6~11讲。
- 6 | x: break x;
这一讲是讲了真正的语句执行。仍然是“不知死,即不知生”的讲法,`break x`与语句的关系,同`delete x`与引用的关系其实差不多。
而且这一讲也提出了“语句以执行的完成状态”为结果这个伏笔要留到第8讲来解开。
- 7 | ${1}
讲述了特殊的可执行结构。如果按照第一讲中所表达的“JavaScript引擎的核心是一个表达式计算的、连续的执行过程”那么将所有显式的、隐式的“执行行为”合起来看才是“执行逻辑”的全体。正如你不了解每一种特殊的可执行结构也就不了解“${1}”为什么是最“晚”出现的语言特性之一。因为它是对其他执行结构的“集大成者”。
当然还有一点特殊之处也是你需要了解的eval(str)是执行语句,`而`${str}`是`执行表达式。本质上来说JavaScript为这两种执行都找到了“执行一个字符串”的模式这仍然是“概念完整性”。
>
NOTE试试如下代码
```
&gt; `${{}}`
'[object Object]'
&gt; eval('{}')
undefined
```
- 8 | x =&gt; x
表面上看是讲一个箭头函数实际上是在讲函数式语言。关键处是解开第6讲伏笔的这一句
>
语句执行是命令式范型的体现而函数执行代表了JavaScript中的对函数式范型的理解。
另外,这一讲把函数分成了三个语法组件:参数、执行体、结果。这是非常重要的一个点,它引导了后面两讲的讨论方式。
- 9 | (…x)
这一讲说的是如何改造函数的三个语法组件中的“执行体”。这一讲提出了“改造三个语法组件”的意义,也就是说,函数式语言无论如何变、语法如何处理,其实本质上,就是在这三个点上做手脚、玩花样。
- 10 | x = yield x
这一讲说的是如何改造函数的三个语法组件中的“参数”和“结果”。
>
NOTE: 这一讲也为将来“再讲循环”留了一个伏笔不过这并不是前20讲的内容这是“更远的将来”。^^.
- 11 | throw 1;
这一讲其实讲的是怎么读ECMAScript规范。
不过它是以“最小化的”三个规范说明来讲述了ECMAScript层面是如何一步一步地将JavaScript搭建出来的。这一讲里面有很多概念和观念一旦你弄明白了对ECMAScript也好JavaScript也好都能起到“点化”的作用。
其实这里有很重要的一点引导,是这样一句话:
>
其中的“result of evaluating…”基本上算是ECMAScript中一个约定俗成的写法不管是执行语句还是表达式都是如此。
这句话很重要它从ECMAScript规范层面、从语句叙述的层面“一致化了”语句执行和表达式执行。注意这就是上面那张图的出处
这是第二模块的内容。 根本上来说承接我们这一模块的总标题“JavaScript是如何运行的”我主要为你讲述了三层概念
- 第三层概念:表达式执行、函数执行、函数执行的扩展。
- 第二层概念:在规范层面如何统一“表达式执行和函数执行”。
- 第一层概念:语言体系的建立。
参考前面的图既然执行结果被统一为“result”且执行被统一为“evaluating”那么运算就被统一成“result of evaluating…”并且结果如果返回给计算系统的外部的话就是一个能被理解的result.value。
>
NOTE: 这个概念层次的构建以及最终对它要达到的效果的预期你可以参考阅读《程序原本》第4.6节,它的标题是:将“计算机程序设计”教成语言课,是本末倒置的。
### 模块三体系3
回顾上面的内容,
- “体系1”说的是“物理到逻辑”的映射
- “体系2”说的是“语言体系的建立”
总体上来看,它们是在陈述一件事情:“抽象的语言”如何处理“物理的代码”。
这仍然是一个体系。
>
<p>NOTE: 回顾前两大模块的标题其实这个“体系3”我是一开始就告诉了你的<br>
从零开始JavaScript语言是如何构建起来的<br>
从表达式到执行引擎JavaScript是如何运行的</p>
## 最后
本来课程设计中并没有今天这一讲的加餐。按原定的计划就是用第11讲最后的“小结”算作引导你的、对之前内容最终回顾了。
但是考虑到课程进度和实际上的难度才有了上一次的和今天的加餐。尤其是今天的内容其实就是对上一讲——第11讲的小结内容的展开希望你能对照着重新来理解和梳理这门课程。
希望这份加餐会让你后续的课程变得轻松一些。今天就到这里,下一讲我们开始讲面向对象。