谈到 JavaScript 你会想到什么呢?浏览器?很好。Node?很好。还有其他的吗?肯定还会有。不过今天,我一不说浏览器,二不说 Node,我要说语言。
「语言这东西由说的必要吗?我每天写那么多程序,经手的代码都超过五位数行了,难道我还不会语言?笑话!还不如给我看 MVC、设计模式、代码案例,那些东西可更有用。」
——且慢——
没错,每个 JS 程序员都必定熟悉 JavaScript 语言,了解它的特性,比如对象、原型、函数以及 this
等等。熟悉到甚么程度?没错,就是到天天用,用成条件反射,好像直接刻进脑子里。但是,你有没有想过,这些「特性」的背后是什么?它们的机理是什么?哦, 你肯定不会想——废话,自己用了这么久的东西自己还不懂?另外,机理有用吗?对于「码农」来说,可能没用,但是不了解某个东西的机理,它的一些问题你就没 法处理,至少没法处理清楚。
比如,许多人都知道细胞呼吸的过程是「燃烧」葡萄糖,产生二氧化碳和水,可以用方程 C 6H 12O 6 + O 2 → CO 2 + H 2O 概括。然而简单的方程无法解释,为什么细胞可以进行无氧呼吸,同样消耗葡萄糖产生能量(尽管数量少于完整的呼吸),却不消耗氧气呢?原来,细胞呼吸实际上 有三个步骤:糖酵解、三羧酸循环和氧化磷酸化。在第一步「糖酵解」中,葡萄糖被分解成更简单的化合物丙酮酸,随后丙酮酸被送入三羧酸循环,得到二氧化碳和 NADH。NADH 在最后一步送入氧化磷酸化,在线粒体内膜上和氧气结合,得到水,这个过程放出的能量形成质子浓差,推动 ATP 合成酶运转合成大量的「能量通货」ATP。整个过程只有第三个步骤消耗氧气;三个步骤里,糖酵解和氧化磷酸化放出能量,三羧酸循环反而消耗能量,因而在无 氧条件下,只有糖酵解可以完成。
从这个例子可以看出,你若是不了解细胞呼吸的机理,你就无法解释无氧呼吸是怎么回事。不了解机理,就无法得心应手。于 JavaScript 也是一样的。不过呢,程序语言的境况要比生物学要简单的多。如果把我们研究的东西比作机器的话,生物学、化学、物理学摆在我们面前的是一个个黑箱,我们必 须费尽心思,用各种实验如光谱、传感器、比较跟踪分析等等才能轻微地撬开这个箱子的一角,从而窥视里面无比精妙的机构;与之相反,编译器、解释器那个箱子 则是完全透明,里面那台精密的机器就在你眼前,你可以直接靠过去看个够。编译器要是开源的话那就更好了,这回连箱子都没了,现在不仅可以看了,连拆都可 以,它已经完全是你手里的玩物了。
不仅如此,语言的设计师们会把这门语言的所有机理(没错——所有)写在一本书里,这本书就是这门语言的规范,那台机器的蓝图。在实践中重要的语言 ——例如 C、C++、Java,它们的规范甚至是国际标准。JavaScript 也不例外:ECMA 组织的 ECMA 262 规范就是 JavaScript 这门语言的标准,也包含了它的全部机理。我可以说,要是读懂了 ECMA 262,完全可以做出一个 JavaScript 解释器出来。
在大多数程序员的眼里,做编译器和解释器的都是一群神奇生物,他们能制作出一个神奇的东西,能让你写的代码「动起来」,就像是魔法师一样。对啊,程 序就是现代魔法,而写编译器的估计就是「现代魔法师」里最神秘的一群了。你可能知道语言有「语法」甚至能写出一个而简单语言的语法定义来,但是叫你写个语 法解析器你就熄火了——从未接触过这个领域,怎么写的出来!我估计这里的各位写一个「计算器」都会比较困难——不是按按钮的计算器,是敲一行表达式出结果 的计算器,而且表达式要支持加减乘除和括弧。(其实按按钮的计算器也会牵扯到语法解析的知识,各位都懂设计模式,知道建立联系吗?)
但语法解析真没你想象中的那么神秘,它实际上就是从句子里提取出含义的过程,只不过各位,你们平常「亲手处理」的字符串,要么表示数据(「配置文 件」),要么表示流程(「自动化脚本」)。如果项目是那种老板甲方天天催,一坏损失几百万的那种的话,我估计,你会,数据上 XML 流程上 Lua,完事。表示数据?XML 各种复杂数据都不在话下,而且类库众多,全球通用;表示流程?Lua 是正经的嵌入式脚本语言,体积小,速度快,功能强大。
所以啊,你不会有机会亲自写个语法解析器的,至少在工作里,不会有的!语法解析器是庞大的编译原理冰山上最简单的一角,光这一角,Grune 和 Jacobs 写了整整一本书,比一本小字典还大;这一角之外的东西,比如编译器后端部分的各种优化,更是卷帙浩繁——而它们还只是编译器技术的部分。程序语言的机理更 多的是建构在编译器技术之外的另一块大陆:语言理论之上。在语言理论的世界里,已经几乎不会出现代码了,全是公式。没错!公式!总之,「语言的机理」对一 般程序员来说,实在是太遥远了,太不可企及了,我们只能像仰望星空一样仰望那群大魔导士。
各位可能穷尽一生也不会亲自写一个编译器,因而有些人就抛出论调说「编译器那一大堆东西,于我无用,不用学!」不过正如我前面说的,原理不掌握,你 应用语言永远不能得心应手,你对程序语言的理解,绝对无法和掌握这个神秘领域的人相提并论,达到相同的高度。例如 JavaScript 的函数调用,你写 object.m(xs)
可以进行正确的「方法调用」,但 f = object.m;
f(xs)
就有问题(f
得不到正确的 this
)。 相比这个问题困扰过很多人吧!一些书籍会为了解释这个现象会把函数调用分成两种,一个是「普通调用」,一个是「方法调用」,然后强调他们在语法和语义上的 区别云云。当然我不是想推翻这句话,它是对的,而且是个相当好的解释;但实际上,按照规范(11.2.3 节),JavaScript 根本就不区分所谓「普通调用」和「方法调用」。下面把 11.2.3 节原样抄下来:
产生式 「CallExpression : MemberExpression Arguments」 依如下方法解释
- 令 ref 为解释 MemberExpression 的结果
- 令 func 为
GetValue(ref)- 令 argList 为解释 Arguments 的结果,它应该是由各个参数组成的列表(见 11.2.4)
- 如果
Type(func)不是 Object,抛出 TypeError 异常- 如果
IsCallable(func)为假,抛出 TypeError 异常- 如果
Type(ref)是 Reference 则:
- 如果
IsPropertyReferernce(ref)为真,则
- 令 thisValue 为
GetBase(ref)- 否则(此时 ref的基底是一个环境记录)
- 令 thisValue 为对
GetBase(ref)调用 ImplicitThisValue 的结果- 否则 ref不是 Reference:
- 令 thisValue 为 undefined
- 返回对 func 调用 [[Call]] 内方法的结果,其中 this 指定为 thisValue,参数表指定为 argList。
产生式 「CallExpression : CallExpression Arguments」 也依相同方法解释,惟第一步里 MemberExpression 换成 CallExpression。
Note
如果 func 是原生 ECMAScript 对象,CallExpression 解释结果一定不会是 Reference 类型;其为宿主对象时是否是 Reference 由实现决定,但如果是,只能是非严格的属性 Referrnce。
上面这些东西直接抄自 ECMA 262,原封未动。这段文字为 CallExpression 语法(JavaScript 里的函数调用)赋予了含义。ECMA 262 里 CallExpression 的语法是两条产生式:
-
- CallExpression
- → MemberExpression Arguments
-
- CallExpression
- → CallExpression Arguments
这两条产生式随后被赋予「函数调用」的语义。所谓「方法调用」所必须的 this
信息实际上是被 MemberExpression 的值携带的。携带 this
信息的东西叫做 Reference,它是解释器使用的一种内部类型,可以记录某个表达式所「关联」的 this
信息。规范里并没有「方法调用」这个说法,「方法调用」这个词汇是为了解释 Reference 造出来的。「方法调用」这个词算是个不错的词,相比规范里精确却叫人难懂的 Reference,「方法调用」和「普通调用」的分野显然要简单得多。毕竟规范要精确、无歧义地定义一门语言,而程序员使用的时候,第一,不需要了解到 如此程度;第二,说的太精确会伤人脑细胞,程序员的脑子可都金贵得很,不能磕着碰着。程序员的脑子是人脑,人脑可以接受不太精确的信息,然后结合经验,形 成你写程序时候的行为准则。编译器则不同,编译器是运行在电脑上的,电脑里的东西必须精确,所以编译器作者看规范看得最勤快;也因为如此,做编译器的人往 往是理解语言更为深入、最理解语言机理的人。
如果说「方法调用」算是个要解释的话,坊间书籍里有些解释就不那么好了,甚至是错误的,比如月影那本大部头《JavaScript 王者归来》里面那个「最长行匹配」就误导了不少人,还有 Douglas 宣称的「JavaScript 里所有数值都是浮点」。这些错处或多或少都是因为对规范不熟悉导致的(虽然 ECMA 262 v3 的佶屈聱牙也要负一般责任)。而它们也是我开始写「JavaScript 原理」的原因。我看规范看了不少次,本身参与了对它的翻译,外加现在在维护 moescript 这个 altjs 语言(编译到 JavaScript 的语言),对 JS 的各种特性,都有所接触。
所以,我打算在这里,第一从严谨的角度出发分析 ECMA 规范和 JavaScript,第二写写这两年来的一些真人经历,主要是 moescript 开发的经历。我希望 jser 们可以追随一个编译器作者和分析者的脚步,来审视你们日复一日使用的东西,看能不能得到一些启发。除开 JS,我也会谈及一些其他的知识和技术,还有少少的个人评价。个人水平有限,还望看官包涵;如果有错误,就大胆的来批好了。
有关「节后三问」
为了让我的书不变成「耳边风」,我会在每一节后面加三个问题。这些题目希望各位认真把它做出来。笔者比照 TAOCP,为每个题目标出了难度。它们如下表列:
- [D] 非常简单,您应该在几分钟内就可以解答出来。
- [C] 比较简单的问题。可能需要好好阅读问题,并且大概要 20 分钟。
- [B] 中等难度的问题。为了完整解答它,你可能需要几个小时的努力。(当然,你的电脑联网的话时间可能更长)。本书中的多数题目都是这个难度。
- [A] 相当困难或者繁杂的题目,或许需要几个星期才能把它做完。大学教授会把它作为学期大作业。
- [论外] 对于这个难度的问题,可能还没有人知道答案。如果你在大学或者研究所的话,你可以把它作为研究方向。如果你把它做出来了,首先应该发篇论文,而不是找我(不过如果我看到阁下的研究成果的话,我会去亲自找您)。
下面是难度的示例:
节后三问
(本回完)
还有下面一系列的教程,关注
http://typeof.net/s/jsmech/02.html
http://typeof.net/s/jsmech/03.html