ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

读懂 ECMAScript 规格:《ECMAScript 6入门》规格阅读指南

2026/9/20 13:55:48 拓冰建站 浏览量
读懂 ECMAScript 规格:《ECMAScript 6入门》规格阅读指南 读懂 ECMAScript 规格《ECMAScript 6入门》规格阅读指南【免费下载链接】es6tutorial《ECMAScript 6入门》是一本开源的 JavaScript 语言教程全面介绍 ECMAScript 6 新增的语法特性。项目地址: https://gitcode.com/gh_mirrors/es/es6tutorial规格文件Specification是计算机语言的官方标准详细描述了语法规则与实现方法。当 JavaScript 遇到语法书里找不到答案的疑难问题时规格文件就是解决问题的最后一招。本文是《ECMAScript 6入门》教程中读懂规格一章的完整展开先带你了解 ES6 规格的整体结构再拆解抽象操作、Record、[[Notation]]、Completion Record 等核心术语最后以相等运算符、数组空位、map()方法三个真实案例演示查规格、读算法、对实现的完整方法论。读完本文你将具备直接阅读 ECMA-262 规格并定位疑难语法问题根因的能力。为什么需要读懂规格规格文件是计算机语言的官方标准详细描述语法规则和实现方法。一般来说没有必要阅读规格除非你要写编译器。因为规格写得非常抽象和精炼又缺乏实例不容易理解而且对于解决实际的应用问题帮助不大。但是如果你遇到疑难的语法问题实在找不到答案这时可以去查看规格文件了解语言标准是怎么说的——规格是解决问题的最后一招。这对 JavaScript 语言尤其必要。它的使用场景复杂语法规则不统一例外很多各种运行环境的行为不一致导致奇怪的语法问题层出不穷任何语法书都不可能囊括所有情况。查看规格不失为一种解决语法问题的最可靠、最权威的终极方法。在《ECMAScript 6入门》教程中读懂规格是全书的第 25 章见 SUMMARY.md。它与教程正文的关系是方法论与知识的关系正文各章如数组的扩展、函数的扩展、数值的扩展讲解 ES6 的语法特性而规格一章教会你如何回到语言标准的源头去验证和深挖这些行为。ES6 规格文件的结构与阅读策略ECMAScript 6 的规格可以在 ECMA 国际标准组织的官方网站www.ecma-international.org/ecma-262/6.0/免费下载和在线阅读对应官方文件为《ECMAScript® 2015 Language Specification》参见参考链接中的官方文件一节。这个规格文件相当庞大一共有 26 章A4 打印的话足足有 545 页。它的特点就是规定得非常细致每一个语法行为、每一个函数的实现都做了详尽的清晰的描述。基本上编译器作者只要把每一步翻译成代码就可以了。这很大程度上保证了所有 ES6 实现都有一致的行为。26 章之中各部分的定位如下第 1 章到第 3 章对文件本身的介绍与语言关系不大第 4 章对这门语言总体设计的描述有兴趣的读者可以读一下第 5 章到第 8 章语言宏观层面的描述。第 5 章是规格的名词解释和写法的介绍第 6 章介绍数据类型第 7 章介绍语言内部用到的抽象操作第 8 章介绍代码如何运行第 9 章到第 26 章介绍具体的语法。对于一般用户来说除了第 4 章其他章节都涉及某一方面的细节不用通读只要在用到的时候查阅相关章节即可。这是一种按需查阅式的阅读策略把规格当成字典而不是教科书。规格术语读懂 ES6 规格的四个关键概念ES6 规格使用了一些专门的术语了解这些术语可以帮助你读懂规格。下面介绍其中最重要的四个。抽象操作abstract operations所谓抽象操作就是引擎的一些内部方法外部不能调用。规格定义了一系列的抽象操作规定了它们的行为留给各种引擎自己去实现。举例来说Boolean(value)的算法第一步是这样的LetbbeToBoolean(value).这里的ToBoolean就是一个抽象操作是引擎内部求出布尔值的算法。许多函数的算法都会多次用到同样的步骤所以 ES6 规格将它们抽出来定义成抽象操作方便描述。抽象操作是规格的词汇表ToBoolean、ToNumber、ToLength、ToPrimitive、Get、HasProperty、Call等都会在后面的算法中反复出现。Record 和 fieldES6 规格将键值对key-value map的数据结构称为Record其中的每一组键值对称为field。也就是说一个 Record 由多个 field 组成而每个 field 都包含一个键名key和一个键值value。可以把它理解为规格内部使用的一种抽象数据结构用来描述对象、属性表等运行时状态。[[Notation]] 内部属性记号ES6 规格大量使用[[Notation]]这种书写法比如[[Value]]、[[Writable]]、[[Get]]、[[Set]]等等。它用来指代 field 的键名。举例来说obj是一个 Record它有一个Prototype属性。ES6 规格不会写obj.Prototype而是写obj.[[Prototype]]。一般来说使用[[Notation]]这种书写法的属性都是对象的内部属性。所有的 JavaScript 函数都有一个内部属性[[Call]]用来运行该函数F.[[Call]](V, argumentsList)上面代码中F是一个函数对象[[Call]]是它的内部方法F.[[Call]]()表示运行该函数V表示[[Call]]运行时this的值argumentsList则是调用时传入函数的参数。Completion Record每一个语句都会返回一个 Completion Record表示运行结果。每个 Completion Record 有一个[[Type]]属性表示运行结果的类型。[[Type]]属性有五种可能的值normalreturnthrowbreakcontinue如果[[Type]]的值是normal就称为 normal completion表示运行正常。其他的值都称为 abrupt completion异常完成。其中开发者只需要关注[[Type]]为throw的情况即运行出错break、continue、return这三个值都只出现在特定场景可以不用考虑。抽象操作的标准流程ReturnIfAbrupt、? 与 !抽象操作的运行流程一般是下面这样LetresultbeAbstractOp().Ifresultis an abrupt completion, returnresult.Setresulttoresult.[[Value]].returnresult.上面的第一步调用了抽象操作AbstractOp()得到result这是一个 Completion Record。第二步如果result属于 abrupt completion就直接返回。如果此处没有返回表示result属于 normal completion。第三步将result的值设置为result.[[Value]]。第四步返回result。ES6 规格将这个标准流程使用简写的方式表达LetresultbeAbstractOp().ReturnIfAbrupt(result).returnresult.这个简写方式里面的ReturnIfAbrupt(result)就代表了上面的第二步和第三步即如果有报错就返回错误否则取出值。甚至还有进一步的简写格式Letresultbe? AbstractOp().returnresult.上面流程的?代表AbstractOp()可能会报错。一旦报错就返回错误否则取出值。也就是说?是ReturnIfAbrupt的浓缩记号。除了?ES6 规格还使用另一个简写符号!Letresultbe! AbstractOp().returnresult.上面流程的!代表AbstractOp()不会报错返回的一定是 normal completion总是可以取出值。这三个符号构成了阅读规格算法的速记三件套?表示可能抛错需传播、!表示保证成功、显式的ReturnIfAbrupt则是未浓缩的完整写法。掌握它们之后规格里那些动辄十几步的算法读起来会顺畅得多。实战案例一相等运算符为什么0 null是 false相等运算符是一个很让人头痛的运算符它的语法行为多变不符合直觉。这个小节就看看规格怎么规定它的行为。请看下面这个表达式请问它的值是多少0 null如果你不确定答案或者想知道语言内部怎么处理就可以去查看规格。规格对每一种语法行为的描述都分成两部分先是总体的行为描述然后是实现的算法细节。相等运算符的总体描述只有一句话The comparisonx y, wherexandyare values, producestrueorfalse.这句话的意思是相等运算符用于比较两个值返回true或false。下面是算法细节ReturnIfAbrupt(x).ReturnIfAbrupt(y).IfType(x)is the same asType(y), thenReturn the result of performing Strict Equality Comparisonx y.Ifxisnullandyisundefined, returntrue.Ifxisundefinedandyisnull, returntrue.IfType(x)is Number andType(y)is String, return the result of the comparisonx ToNumber(y).IfType(x)is String andType(y)is Number, return the result of the comparisonToNumber(x) y.IfType(x)is Boolean, return the result of the comparisonToNumber(x) y.IfType(y)is Boolean, return the result of the comparisonx ToNumber(y).IfType(x)is either String, Number, or Symbol andType(y)is Object, then return the result of the comparisonx ToPrimitive(y).IfType(x)is Object andType(y)is either String, Number, or Symbol, then return the result of the comparisonToPrimitive(x) y.Returnfalse.上面这段算法一共有 12 步翻译如下如果x不是正常值比如抛出一个错误中断执行。如果y不是正常值中断执行。如果Type(x)与Type(y)相同执行严格相等运算x y。如果x是nully是undefined返回true。如果x是undefinedy是null返回true。如果Type(x)是数值Type(y)是字符串返回x ToNumber(y)的结果。如果Type(x)是字符串Type(y)是数值返回ToNumber(x) y的结果。如果Type(x)是布尔值返回ToNumber(x) y的结果。如果Type(y)是布尔值返回x ToNumber(y)的结果。如果Type(x)是字符串或数值或Symbol值Type(y)是对象返回x ToPrimitive(y)的结果。如果Type(x)是对象Type(y)是字符串或数值或Symbol值返回ToPrimitive(x) y的结果。返回false。由于0的类型是数值null的类型是 Null这是规格中Null 类型的规定是内部Type运算的结果跟typeof运算符无关。因此上面的前 11 步都得不到结果要到第 12 步才能得到false0 null // false这个案例很好地演示了规格阅读的完整路径遇到疑问 → 定位相关小节 → 读总体描述 → 逐条翻译算法 → 得出结论。规格中 12 步算法里第 8、9 步对布尔值的ToNumber转换、第 10、11 步对对象的ToPrimitive转换正是各种不符合直觉行为如null 0为 false、[] 0为 true的根源而这些行为在普通语法书中往往被一笔带过。实战案例二数组的空位hole为什么与 undefined 不同再看另一个例子const a1 [undefined, undefined, undefined]; const a2 [, , ,]; a1.length // 3 a2.length // 3 a1[0] // undefined a2[0] // undefined a1[0] a2[0] // true上面代码中数组a1的成员是三个undefined数组a2的成员是三个空位。这两个数组很相似长度都是 3每个位置的成员读取出来都是undefined。但是它们实际上存在重大差异0 in a1 // true 0 in a2 // false a1.hasOwnProperty(0) // true a2.hasOwnProperty(0) // false Object.keys(a1) // [0, 1, 2] Object.keys(a2) // [] a1.map(n 1) // [1, 1, 1] a2.map(n 1) // [, , ,]上面代码一共列出了四种运算数组a1和a2的结果都不一样。前三种运算in运算符、数组的hasOwnProperty方法、Object.keys方法都说明数组a2取不到属性名最后一种运算数组的map方法说明数组a2没有发生遍历。为什么a1与a2成员的行为不一致数组的成员是undefined或空位到底有什么不同规格中《数组的初始化》Array Initializer一节给出了答案Array elements may be elided at the beginning, middle or end of the element list. Whenever a comma in the element list is not preceded by an AssignmentExpression (i.e., a comma at the beginning or after another comma), the missing array element contributes to the length of the Array and increases the index of subsequent elements. Elided array elements are not defined. If an element is elided at the end of an array, that element does not contribute to the length of the Array.翻译如下数组成员可以省略。只要逗号前面没有任何表达式数组的length属性就会加 1并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员则不会导致数组length属性增加。上面的规格说得很清楚数组的空位会反映在length属性也就是说空位有自己的位置但是这个位置的值是未定义即这个值是不存在的。如果一定要读取结果就是undefined因为undefined在 JavaScript 语言中表示不存在。这就解释了为什么in运算符、数组的hasOwnProperty方法、Object.keys方法都取不到空位的属性名因为这个属性名根本就不存在规格里面没说要为空位分配属性名位置索引只说要为下一个元素的位置索引加 1。这个案例在《ECMAScript 6入门》的数组的扩展一章中有更完整的延伸教程指出空位不是undefined某一个位置的值等于undefined依然是有值的。空位是没有任何值in运算符可以说明这一点并系统对比了各方法对空位的处理——forEach()、filter()、reduce()、every()、some()会跳过空位map()跳过空位但保留该值join()和toString()将空位视为undefined而 ES6 的Array.from()、扩展运算符...、fill()、for...of循环则会把空位当作普通位置处理。由于空位的处理规则非常不统一教程给出的实用建议是尽量避免在代码中出现数组空位。实战案例三Array.prototype.map 为什么会跳过空位规格中定义了数组的map方法。该小节先是总体描述map方法的行为里面没有提到数组空位。后面的算法描述是这样的LetObeToObject(this value).ReturnIfAbrupt(O).LetlenbeToLength(Get(O, length)).ReturnIfAbrupt(len).IfIsCallable(callbackfn)isfalse, throw a TypeError exception.IfthisArgwas supplied, letTbethisArg; else letTbeundefined.LetAbeArraySpeciesCreate(O, len).ReturnIfAbrupt(A).Letkbe 0.Repeat, whileklenLetPkbeToString(k).LetkPresentbeHasProperty(O, Pk).ReturnIfAbrupt(kPresent).IfkPresentistrue, thenLetkValuebeGet(O, Pk).ReturnIfAbrupt(kValue).LetmappedValuebeCall(callbackfn, T, «kValue, k, O»).ReturnIfAbrupt(mappedValue).LetstatusbeCreateDataPropertyOrThrow (A, Pk, mappedValue).ReturnIfAbrupt(status).Increasekby 1.ReturnA.翻译如下得到当前数组的this对象如果报错就返回求出当前数组的length属性如果报错就返回如果 map 方法的参数callbackfn不可执行就报错如果 map 方法的参数之中指定了this就让T等于该参数否则T为undefined生成一个新的数组A跟当前数组的length属性保持一致如果报错就返回设定k等于 0只要k小于当前数组的length属性就重复下面步骤设定Pk等于ToString(k)即将k转为字符串设定kPresent等于HasProperty(O, Pk)即求当前数组有没有指定属性如果报错就返回如果kPresent等于true则进行下面步骤设定kValue等于Get(O, Pk)取出当前数组的指定属性如果报错就返回设定mappedValue等于Call(callbackfn, T, «kValue, k, O»)即执行回调函数如果报错就返回设定status等于CreateDataPropertyOrThrow (A, Pk, mappedValue)即将回调函数的值放入A数组的指定位置如果报错就返回k增加 1返回A仔细查看上面的算法可以发现当处理一个全是空位的数组时前面步骤都没有问题进入第 10 步中第 2 步时kPresent会得到false因为空位对应的属性名对于数组来说是不存在的。由于回调函数只在kPresent为true时才会被调用第 10.4 步空位就被整个跳过了const arr [, , ,]; arr.map(n { console.log(n); return 1; }) // [, , ,]上面代码中arr是一个全是空位的数组map方法遍历成员时发现是空位就直接跳过不会进入回调函数。因此回调函数里面的console.log语句根本不会执行整个map方法返回一个全是空位的新数组。值得注意的是这一步的关键是规格算法第 10.4 步的kPresent即HasProperty(O, Pk)的返回值——这正是前一节《数组的空位》的结论在map算法中的直接体现空位不拥有属性名HasProperty返回false于是循环体直接进入k加 1。两个案例在此汇合形成了规格内部互相印证的阅读体验。与引擎实现的对照V8 的 ArrayMap规格的价值不仅在于规定行为还在于它直接决定了引擎的实现结构。V8 引擎对map方法的实现如下可以看到跟规格的算法描述完全一致function ArrayMap(f, receiver) { CHECK_OBJECT_COERCIBLE(this, Array.prototype.map); // Pull out the length so that modifications to the length in the // loop will not affect the looping and side effects are visible. var array TO_OBJECT(this); var length TO_LENGTH_OR_UINT32(array.length); return InnerArrayMap(f, receiver, array, length); } function InnerArrayMap(f, receiver, array, length) { if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f); var accumulator new InternalArray(length); var is_array IS_ARRAY(array); var stepping DEBUG_IS_STEPPING(f); for (var i 0; i length; i) { if (HAS_INDEX(array, i, is_array)) { var element array[i]; // Prepare break slots for debugger step in. if (stepping) %DebugPrepareStepInIfStepping(f); accumulator[i] %_Call(f, receiver, element, i, array); } } var result new GlobalArray(); %MoveArrayContents(accumulator, result); return result; }对照可以发现 V8 实现与规格算法的对应关系TO_OBJECT(this)对应规格第 1 步ToObject(this value)TO_LENGTH_OR_UINT32(array.length)对应规格第 3 步ToLength(Get(O, length))if (!IS_CALLABLE(f))对应规格第 5 步的IsCallable检查循环中的if (HAS_INDEX(array, i, is_array))正是规格第 10.4 步的HasProperty(O, Pk)判断只有存在属性索引才读取元素、调用回调并写入累加器accumulator[i] %_Call(f, receiver, element, i, array)对应规格第 10.4.c 步Call(callbackfn, T, «kValue, k, O»)。也就是说规格中每一步抽象算法都可以在引擎源码中找到对应的实现行。这正是规格基本上编译器作者只要把每一步翻译成代码就可以了这一特点的具体体现也是所有 ES6 实现行为一致性的根本保证。把规格阅读融入日常开发从上面三个案例可以看到阅读规格并不是编译器的专利。当你遇到以下场景时规格就是你最可靠的参考遇到不符合直觉的运算符行为如的类型转换直接查对应运算的算法小节逐条对照即可定位答案需要区分看起来一样的数据形态如undefined成员与数组空位规格对是否定义了属性给出了明确的判定标准HasProperty想理解某个方法为什么这样做如map跳过空位、forEach与map的差异规格的算法步骤就是最权威的行为说明书还可以进一步与 V8 等引擎源码互相印证。阅读规格时建议掌握以下几个要点先定位章节ES6 规格第 58 章是宏观术语与抽象操作第 926 章是具体语法按需查阅不必通读先总述后算法每个语法行为都分总体描述 算法细节两部分先用总体描述建立直觉再逐条翻译算法掌握速记符号?表示可能报错并传播、!表示保证成功、ReturnIfAbrupt是完整的显式写法用代码验证阅读算法得出的结论最终都要回到 Node.js 或浏览器控制台用代码验证一遍形成规格 → 实现 → 验证的闭环。本教程的其他章节同样与规格有着深层的联系。例如数值的扩展中Number.EPSILON的定义就根据规格表示 1 与大于 1 的最小浮点数之间的差函数的扩展中提到 ES6 第一次明确规定所有 ECMAScript 实现都必须部署尾调用优化这正是规格对引擎行为的强制性规定。理解了规格你就等于拿到了理解这些语法设计动机的钥匙。【免费下载链接】es6tutorial《ECMAScript 6入门》是一本开源的 JavaScript 语言教程全面介绍 ECMAScript 6 新增的语法特性。项目地址: https://gitcode.com/gh_mirrors/es/es6tutorial创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考