查看原文
其他

手把手带你读 ECMAScript 规范 Part 1

前端食堂 2021-01-14

The following article is from 淘系前端团队 Author 昭朗

文末福利:开发者藏经阁


即使我们非常熟悉 JavaScript,因为 ECMAScript 规范篇幅巨长(全篇将近 60 万词),读起来也不是一个容易的事情,更何况浏览器/阅读器初次打开都需要加载好一会儿的内容长度不仅会吓退很多人阅读的小冲动,也让人难以维持继续读下去的动力。那我们为什么需要阅读 ECMAScript 规范?


后文我们使用 ECMAScript 指代由 Ecma International Technical Committee 39 负责编撰的 ECMAScript Language Specification,而使用 JavaScript 来指代我们日常使用的那个常见编程语言。


NO.1

为什么我们需要阅读 ECMAScript 规范?


ECMAScript 规范作为众多浏览器、Node.js 的 JavaScript 引擎的行为实现标准,要了解 JavaScript 是以什么样具体的行为运行的,我们就需要靠解读 ECMAScript 规范所定义的操作步骤来了解其中的详情。


比如有时我们会使用 Array.prototype 上的内置方法来进行一些便捷操作,但是也会出现一些让人迷惑的现象,如:


Array.prototype.push('foo')
1
Array.isArray(Array.prototype)
true
Set.prototype.add('foo')
Uncaught TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
    at Set.add (<anonymous>)


如果我们在实际使用中碰到了这样的问题,平时最可能有贡献的Google 可能并帮不上什么忙,而 Stackoverflow 也并不能提供更多的帮助,这时,我们可以找到最相关的一个文档来源就是 ECMAScript 规范文本了,其中包含了对 Array.prototype.push 操作的详细步骤,和 Array Prototype Object 的属性定义,通过这些文档,我们就可以知道上述的代码中具体发生了些什么,为什么最后的行为表现会是如此结果。


或者,如果我们希望了解 ===== 之间具体的差别,或许我们可以通过阅读 MDN 上的平淡的介绍(可能读半天也没找到差别具体在哪里),但是通过阅读 ECMAScript 规范文本对两个操作符的具体定义,如 == 运算符的运行时语义小节,我们知道 == 操作符是通过 Abstract Equality Comparison 操作计算的结果,继续带着问题查找下去,我们就可以了解这其中具体发生了什么,每一种运算符中具体干了什么才会导致目前我们使用的 ===== 有行为差别,也可以解释为什么通常代码 Lint 规则会禁止使用 == 操作符,但是又通常开放 == null 的特例。


规范的标准化与 ECMAScript 规范的测试集 test262 促成我们在不同的 JavaScript 运行环境中同样的 JavaScript 代码都能获得预期的同样结果,也将 JavaScript 语言的语义细节都使用避免歧义的标准文本在规范中详细地记录了下来,让我们更加容易理解 JavaScript 的行为。


NO.2

ECMAScript 规范包含了什么内容?


如果有人问我们,JavaScript 是什么,“JavaScript 就是由各种 JavaScript 的特性组成的编程语言”这样的回答并不能消除提问者的疑问。ECMAScript 应该包含 JavaScript 的哪些部分特性?什么特性会被归于 JavaScript?ECMAScript 这么长的篇幅,是不是在其中包含了我们日常使用 JavaScript 时所碰到的看似都是 JavaScript 语言特性的定义?


JavaScript 是少有的会严格区分语言特性与宿主环境能力的语言,现在有多种不同知名的宿主环境如浏览器、服务端、嵌入式设备,都会使用嵌入的不同 JavaScript 实现提供计算、数据操作环境。作为为了在一个宿主环境里执行计算、操作数据的面向对象语言而设计的语言,ECMAScript 规范并没有以一个完整自给自足的语言设计为目标,ECMAScript 规范里没有对与输入外部数据亦或是将计算结果输出的定义。各个 JavaScript 的宿主环境通常通过添加能被全局访问的对象来为 JavaScript 提供访问宿主环境 API 与 I/O 的能力,如 documentXMLHttpRequestprocessrequire 等。而这些行为的定义虽然不会被包含在 ECMAScript 规范的范畴之中,ECMAScript 规范中会说明宿主环境可以提供某些特性来被 JavaScript 程序访问、使用。


特性ECMAScript 规范范畴
语法元素的语法定义,如如何写一个符合规范的 for-in 循环
语法元素的语义定义,如 typeof 操作符的定义,语句 { foo: 'bar' } 的返回值
Object, Array, Proxy 等等内置对象的方法例程
import a from 'a.mjs'⭕️,注解1
console, setTimeout, clearTimeout❌,注解2
Buffer, process, global❌,注解3
module, exports, require(), __filename, __dirname❌,注解4
window, alert, XMLHttpRequestdocument 等 DOM 对象等❌,注解5


注解1

ECMAScript 规范中定义了 ECMAScript Module 的语法与所表达的含义,但是并不包含一个 JavaScript 运行环境应该如何加载这些被依赖模块,如 Node.js 与浏览器环境所提供的 ECMAScript Module 有不同的模块解析算法。


注解2

这些方法在浏览器环境与 Node.js 环境中都存在,但是这些方法与他们所涉及到的 IO 操作与语义是 JavaScript 运行环境提供的,而不是在 ECMAScript 规范中定义的。


注解3

这些方法与对象和他们所涉及到的 IO 操作与语义是 Node.js 环境提供的,而不是在 ECMAScript 规范中定义的。


注解4

Node.js 环境提供这些 CommonJS 模块所定义的对象与方法,而不是在 ECMAScript 规范中定义的。


注解5

这些浏览器与 DOM API 与他们提供的 IO 操作与语义是由 W3C 工作组定义的规范,而不是在 ECMASCript 规范中定义的。


NO.3

如何获取 ECMAScript 规范?


我们可以在 https://tc39.es/ecma262 获取到最新的 ECMAScript 规范。


在 ECMAScript 第五版通过标准文本将 JavaScript 的事实标准书面标准化之后,Ecma TC39 采用了以年为周期的 ECMAScript 标准更新编撰节奏,ECMAScript 第六版即是这个以节奏发布的第一个版本。ECMAScript 的规范纯文本源码公开在 https://github.com/tc39/ecma262,同时对标准文本的编撰,如问题修复,编辑性修复(用词等问题)等等优化、改进,都通过公开的 Pull Request 进行标准的修订流程。而后每一年 TC39 在一个时间点会将当时已经完成编撰的 ECMAScript 规范归档保存,标注上一个版本号后即可作为当年的 ECMAScript 语言标准发布了。如 ECMAScript® 2019 Language Specification (ECMA-262, 10th edition)(或者叫做 ES10,ES2019)就是在 2019 年 6 月时在 https://tc39.es/ecma262 可以看到的 ECMA 262 标准文本,通过一定的包装或 PDF 格式化,用作永久保存。


NO.4

规范导览


ECMAScript 规范的内容可以分成以下几个部分:


  • 惯例与基础:如在 ECMAScript 中 Number 的定义是什么,亦或者 throw a TypeError exception 语句代表什么含义;

  • 语言语法产生式:如如何写一个符合规范的 for-in 循环;

  • 语言静态语义:如一个 VariableDeclaration 如何确定一个变量声明;

  • 语言运行时语义:如一个 for-in 循环的执行例程的定义;

  • APIs:如 String.prototype.substring 等内置对象的方法例程定义。


当然 ECMAScript 规范的内容并不是按照以上的顺序书写的,上述的内容通常在不同的章节交叉着说明。也因为 ECMAScript 规范篇幅非常长,通常也不会有人会从上到下完整通读,而对大部分人来说也没有这个必要,这样读的效果也不会尽人意。我们可以在我们日常书写 JavaScript 的过程当中,碰到了一个具体问题之后,再带着这个问题,想一想这个问题应该是上述中的哪一部分,然后再去 ECMAScript 规范中去寻找相关的定义文本。如果在这个过程中我们发现难以确定这个问题是属于哪一部分,可以考虑一下“这个(问题)是在哪一个阶段被执行的?”,就可以比较容易确定 ECMAScript 规范中的段落了。


下面我们将通过一个问题来深入 ECMAScript 规范文本,看看日常工作中我们所使用的 JavaScript 是如何按照规范文本的定义执行的。


NO.5

基本类型的属性访问


JavaScript 对象上的成员属性广为人知是通过遍历原型链查找的,比如 ({}).hasOwnProperty 中虽然对象字面量上没有定义 hasOwnProperty 成员,但是因为对象字面量的原型默认就是 Object.prototype,所以 Object.prototype 上的 hasOwnProperty 成员也就可以从这个对象字面量上访问到了。那我们经常会在一些基本类型值上也会对其的属性进行访问,那这些属性又是在哪儿定义的呢?难道基本类型也有原型定义吗?


'foobar'.substring(3);
// -> 'bar'


注意:下文中会有许多对 ECMAScript 规范文本的直接引用,截止我们撰文的时间 2020 年 3 月 10 日,在此之后最新版本的 ECMAScript 规范文本可能会有更新,在阅读的时候可以参考最新规范文本阅读。


NO.6

哪儿定义了成员属性访问的语法?


从成员表达式的文法生成式可以看到,成员表达式有 7 个可能的生成式。成员表达式可以是一个单独的 PrimaryExpression,也可以是一个成员表达式加上一个由方括号包裹的 Expression:MemberExpression [ Expression ],比如 obj['foo']


MemberExpression:
    PrimaryExpression
    MemberExpression [ Expression ]
    MemberExpression . IdentifierName
    MemberExpression TemplateLiteral
    SuperProperty
    MetaProperty
    new MemberExpression Arguments


'foobar'.substring 即是 MemberExpression . IdentifierName 所表达的文法。回到我们的问题,“基本类型的属性是如何访问的?”,属性访问是发生在运行时的,那么我们可以先来看看这段成员表达式的运行时语义。

了解更多上下文无关文法:https://en.wikipedia.org/wiki/Context-free_grammar


成员表达式的运行时语义


语法的运行时语义定义了这个通过这个语法的定义解析完成后,在运行时是如何表达他的含义的,比如成员表达式的运行时语义中定义了上文中 MemberExpression . IdentifierName 生成式如 foo.bar 这样的表达式在运行时是如何在 foo 上取出他的成员属性 bar 的值。


大多数 ECMAScript 规范中的运行时语义是由一系列算法步骤组成的,不过不像常规的伪代码,会使用更加精确的方式描述操作步骤。


MemberExpression : MemberExpression . IdentifierName
1. Let baseReference be the result of evaluating MemberExpression.
2. Let baseValue be ? GetValue ( baseReference ).
3. If the code matched by this MemberExpression is strict mode code , let strict be trueelse let strict be false.
4. Return ? EvaluatePropertyAccessWithIdentifierKey ( baseValue , IdentifierName , strict ).


可以看到操作的第 4 步将更多具体的操作代理给了另外一个抽象操作 EvaluatePropertyAccessWithIdentifierKey


EvaluatePropertyAccessWithIdentifierKey ( baseValue, identifierName, strict )
1. Assert: identifierName is an IdentifierName.
2. Let bv be ? RequireObjectCoercible ( baseValue ).
3. Let propertyNameString be StringValue of identifierName.
4. Return a value of type Reference whose base value component is bv , whose referenced name component is propertyNameString, and whose strict reference flag is strict.

这段算法返回了一个引用类型,并且没有对对象执行任何具体的操作,那么这个属性引用类型是如何转换成具体的值的呢?我们回到我们的例子,可以发现,除了 'foobar'.startsWith 这段属性访问之外,代码中还有一次函数调用:


'foobar'.startsWith('foo');


引用类型常常被用在像 delete,typeof,赋值操作,super 关键字等等特性中。比如赋值操作的左操作 obj.foo = 'bar'obj.foo 就是一个引用类型,只有在最终的赋值操作中,引用才会被真正地具像化。

引用类型包含解析后的名字或者属性绑定。单个引用包含三个部分,引用基底,引用名,与是否是严格引用 flag。引用基底通常会是 undefined,一个对象,布尔值,字符串,Symbol,数字,BigInt,或者是 Environment Record。如果基底是 undefined 代表这个引用无法被解析。引用名会是字符串或者 Symbol,即我们能在 JavaScript 中使用的键类型。


所以我们继续查看以下调用表达式代表的运行时语义。


CallExpression : CoverCallExpressionAndAsyncArrowHead
1. Let expr be CoveredCallExpression of CoverCallExpressionAndAsyncArrowHead.
2. Let memberExpr be the MemberExpression of expr.
3. Let arguments be the Arguments of expr.
4. Let ref be the result of evaluating memberExpr.
5. Let func be ? GetValue(ref).
6. If Type(ref) is Reference, IsPropertyReference(ref) is false, and GetReferencedName(ref) is "eval", then
    a. If SameValue(func, %eval%) is true, then
        i. Let argList be ? ArgumentListEvaluation of arguments.
        ii. If argList has no elements, return undefined.
        iii. Let evalArg be the first element of argList.
        iv. If the source code matching this CallExpression is strict mode code, let strictCaller be true. Otherwise let strictCaller be false.
        v. Let evalRealm be the current Realm Record.
        vi. Return ? PerformEval(evalArg, evalRealm, strictCaller, true).
7. Let thisCall be this CallExpression.
8. Let tailCall be IsInTailPosition(thisCall).
9. Return ? EvaluateCall(func, ref, arguments, tailCall).


调用表达式的运行时语义抽象操作中,在第 5 步通过 GetValue 获取 MemberExpression 运行时语义抽象操作返回的引用类型表达的值。


上文中我们提到了“抽象操作”这个词,抽象操作的写法 OperationName(arg1, arg2) 与函数类似,也可以接受一个或多个参数,而他们与普通 JavaScript 函数不同的是,他们不能被在 JavaScript 中直接访问到,只是作为一个书写惯例,便于在 ECMAScript 规范文本中重复利用一系列操作与算法。


Records & Completion Records


在前文的抽象操作中我们会注意到,其中的部分操作前会有 ? 记号,这个记号代表了什么含义?


部分规范中定义的操作就与 ECMAScript 函数一样,需要处理各种控制流不同的表现行为,如通过 throws 关键字中断的执行,并附带一个异常值 Error,或者通过 return 关键字中断函数的执行,并返回一个返回值一样,在 ECMAScript 规范中就是通过 Completion Record 类型来表达不同情况与他们附带的值的。


Records 类型是一个只在 ECMAScript 规范中使用的用来表达包含一系列数据的抽象类型,就如同抽象操作一样,不同的 JavaScript 引擎可以有不同的实现来代表 Record 类型。Record 值可以包含一个或多个键值对,这些键值对的值可以是普通 EMCAScript 值或者是其他 ECMAScript 中定义的抽象类型。在规范文本中,通常会使用双方括号的写法 [[Field]] 来代表对 Record 的字段访问。


Completion 类型作为一个具体的 Record 类型,下表就是 Completion Record 定义的键值对。

Field NameValueMeaning
[[Type]]One of normal, break, continue, return, or throwThe type of completion that occurred.
[[Value]]any ECMAScript language value or emptyThe value that was produced.
[[Target]]any ECMAScript string or emptyThe target label for directed control transfers.


[[Type]] 是 normal 的 Completion Record 就可以叫做 Normal Completion,而除了 Normal Completion 之外的 Completion 类型都可以称为 Abrupt Completion。大部分时候,我们只会碰到 [[Type]] 为 throw 的 Abrupt Completion。其他三个 Abrupt Completion 只会在一些具体的文法元素被执行的时候才会出现。


在 ECMAScript 规范文本定义中,并不会出现类似 JavaScript 代码中的 try-catch 代码块,每一个可能的错误情况(或者是 Abrupt Completion)都需要被显示地处理。而如果没有一些便捷手段来处理这些情况,所有抽象操作中对错误的处理都需要写成以下四个步骤:先获取返回值;再在第二步中判断这个返回的 CompletionRecord 是不是一个 Abrupt Completion,如果是的话,就将这个 Abrupt Completion 作为这次操作的返回值返回;第三步从 CompletionRecord 中获取包裹的返回值;第四部才能开始我们真正的处理。就像下面这段描述一样:


1. Let resultCompletionRecord be AbstractOp().
2. If resultCompletionRecord is an abrupt completion, return resultCompletionRecord.
3. Let result be resultCompletionRecord.[[Value]].
4. result is the result we need. We can now do more things with it.


在 ES2016 以后,规范中就新增了几个简洁的写法,以上同样的文本可以写成下面 3 个步骤,其中第 2 步与第 3 步通过 ReturnIfAbrupt 处理所有的 Abrupt Completion,并自动将 result 的 [[Value]] 解包。


1. Let result be AbstractOp().
2. ReturnIfAbrupt(result).
3. result is the result we need. We can now do more things with it.


更进一步,通过引入 ? 记号,操作的描述就完全不再需要处理 CompletionRecord,而 result 已经是 [[Value]] 解包后的值了。


1. Let result be ? AbstractOp().
2. result is the result we need. We can now do more things with it.


? 记号类似,在 ECMAScript 规范文本中也会出现 ! 记号,这相当于对于这个操作返回值断言返回值必须是 Normal Completion。


1. Let val be ! OperationName().

// 相当于 ⬇️

1. Let val be OperationName().
2. Assert: val is never an abrupt completion.
3. If val is a Completion Record, set val to val.[[Value]].


可以在 ECMAScript 规范中的 ReturnIfAbrupt 简写符号 了解更多相关内容。


对象内部槽位


回到属性访问操作,CallExpression 在运行时需要对一个具体的函数进行函数调用操作,所以在第 5 步通过 GetValue 获取 MemberExpression 返回的引用类型对应的值:


GetValue ( V )
1. ReturnIfAbrupt(V).
2. If Type(V) is not Reference, return V.
3. Let base be GetBase(V).
4. If IsUnresolvableReference(V) is truethrow a ReferenceError exception.
5. If IsPropertyReference(V) is true, then
    a. If HasPrimitiveBase(V) is true, then
        i. Assert: In this case, base will never be undefined or null.
        ii. Set base to ! ToObject(base).
    b. Return ? base.[[Get]](GetReferencedName(V), GetThisValue(V)).
6. Else,
    a. Assert: base is an Environment Record.
    b. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V)) (see 8.1.1).


对比我们的例子代码可以看到,如果属性引用的基底是一个基本类型值,那么其中的步骤 5.a 就会对其执行 ToObject 抽象操作,因为基本类型实际上不像一个对象,有格子用于存储方法的内部存储,可以对各种操作进行重写,所以需要先将其转换成一个以如 String.prototype 为原型的对象,再对其取属性。ToObject 会根据参数的类型进行不同的操作,如对我们例子中的字符串基本类型,根据定义即是创建一个新的以参数中的字符串为数据源的 String 对象并返回。


ToObject ( argument )

Argument TypeResult
UndefinedThrow a TypeError exception.
NullThrow a TypeError exception.
BooleanReturn a new Boolean object whose [[BooleanData]] internal slot is set to argument. See 19.3 for a description of Boolean objects.
NumberReturn a new Number object whose [[NumberData]] internal slot is set to argument. See 20.1 for a description of Number objects.
StringReturn a new String object whose [[StringData]] internal slot is set to argument. See 21.1 for a description of String objects.
SymbolReturn a new Symbol object whose [[SymbolData]] internal slot is set to argument. See 19.4 for a description of Symbol objects.
BigIntReturn a new BigInt object whose [[BigIntData]] internal slot is set to argument. See 20.2 for a description of BigInt objects.
ObjectReturn argument.


也就是说 GetValue 会对基本类型值转换成对象后,再对这个对象进行成员属性的访问,而对对象的成员属性访问即是 GetValue 步骤 5.b 中可以看到是通过 [[Get]] 这个操作,那么这是一个什么样的操作呢?[[这个记号]] 又代表了什么意思?


上文我们提到,ECMAScript 中访问 Record 类型的某个键值对就是使用 [[这个记号]] 的,除此之外,ECMAScript 中对对象的内部槽位与内部方法的访问也是通过类似的记号,到底是哪一种取决于使用的上下文中记号出现的位置,不过可以确定的是,通过 [[这个记号]] 访问属性是我们在 JavaScript 中都无法访问、观察到的属性。


在 ECMAScript 中,每一个 Object 都有一系列的内部方法,这些方法经常会在 ECMAScript 中定义的其他各种抽象操作中被调用。常见的有如:


  • [[Get]],用来获取对象上的一个成员属性(如 obj.prop);

  • [[Set]],用来给对象上的一个成员属性赋值(如 obj.prop = 42);

  • [[GetPrototypeOf]],用来获取对象的原型(如 Object.getPrototypeOf(obj));

  • [[GetOwnProperty]],用来获取对象的自有属性的属性描述符(如 getOwnPropertyDescriptor(obj, "prop"));

  • [[Delete]],用来删除对象上的一个属性(如 delete obj.prop)。


而函数就是一些有额外的 [[Call]] 内部方法的对象(还可以有 [[Construct]] 内部方法),因此函数也可以称为可调用的对象。


除了这些内部方法,JavaScript 对象还有很多内部槽位,这些槽位就是 ECMAScript 规范中用来存储对象的数据的地方。比如大多数对象都有的 [[Prototype]],值得注意的是我们刚提到了 [[GetPrototypeOf]] 这个内部方法,那这两个有什么区别?大多数对象有 [[Prototype]] 内部槽位,但所有对象都会实现 [[GetPrototypeOf]] 内部方法。比如 Proxy 对象并没有他们自己的 [[Prototype]] 内部槽位,但是他们实现了 [[GetPrototypeOf]] 内部方法,这个内部方法会将调用代理给注册的 handler 或者代理对象的 [[GetPrototypeOf]]

可以在 Ordinary Object Internal Methods and Internal Slots 了解更多详细的 Object 内部方法。


可以在 Proxy Object Internal Methods and Internal Slots 了解更多关于 Proxy 外部对象的内部方法。


另外,ECMAScript 还将所有的对象分为两个类型,分别是普通对象和外部对象。大多数我们使用的对象都是普通对象,这意味着这些对象的内部方法都是在 Ordinary Object Internal Methods and Internal Slots 中定义的默认方法。除此之外,我们还使用了非常多种类型的外部对象,这些对象会重新定义许多普通对象默认的内部方法,比如我们对 Array 类型使用下标赋值时 arr[1] = 123 或者 arr.length = 100,就会使用到 Array 外部对象类型重新定义的 [[DefineOwnProperty]] 对这个对象产生额外的操作,如数组扩缩容等。

可以在 Array Exotic Objects 了解更多关于 Array 外部对象的内部方法。


我们可以通过下图更好地了解这些对象的关系。



图:https://timothygu.me/es-howto


回到 GetValueGetValue 在将基础类型转换成普通对象后,在步骤 5.b 中通过调用对象的 [[Get]] 内部方法来获取对象的属性值:


`[[Get]]` ( P, Receiver )
1. Return ? OrdinaryGet(O, P, Receiver).


可以看到普通对象的 [[Get]] 操作将具体的内容代理给了 OrdinaryGet 抽象操作来处理。而通过 OrdinaryGet 抽象操作我们就可以不断地遍历对象与他的原型链上的所有原型对象的属性,直到找到期望的属性为止(步骤 3,如果没有找到属性,就继续调用原型的 [[Get]] 方法)。


OrdinaryGet ( O, P, Receiver )
1. Assert: IsPropertyKey(P) is true.
2. Let desc be ? O.[[GetOwnProperty]](P).
3. If desc is undefined, then
    a. Let parent be ? O.[[GetPrototypeOf]]().
    b. If parent is nullreturn undefined.
    c. Return ? parent.[[Get]](P, Receiver).
4. If IsDataDescriptor(desc) is truereturn desc.[[Value]].
5. Assert: IsAccessorDescriptor(desc) is true.
6. Let getter be desc.[[Get]].
7. If getter is undefinedreturn undefined.
8. Return ? Call(getter, Receiver).

其中,Property Descriptor 类型 也是一个 Record 类型,在 JavaScript 里我们通常使用对象字面量表示,如 Object.defineProperty(obj, 'foo', { enumerable: true, configurable: false, value: 'bar' })


const it = 'foobar';
'foobar'.substring(3);
// -> 'bar'


到此为止,我们就可以总结出,'foobar' 在属性 substring 的访问过程中被转换成了一个 String 对象,然后通过 String.prototype 获取到 String.prototype.substring,最后通过以过程中获得的 String 对象为 receiver,3 为参数调用这个函数我们就可以得到刚开始例子中的 "bar" 了。


NO.7

String.prototype.substring


在获取到了 String.prototype.substring 这个函数后,如果我们对其调用并使用 undefined 作为 receiver(函数调用的 this 值)会发生什么?


String.prototype.substring.call(undefined24)


我们根据以往 JavaScript 的使用经验,推测大概有两种可能:


  • String.prototype.substring()undefined 转换成字符串类型 "undefined",然后取这个字符串的索引为 2 的字符到索引为 4 的字符(即索引为 [2, 4) 的范围),即最后结果为 "de"。

  • String.prototype.substring() 抛出一个错误,拒绝以 undefined 作为 Receiver 输入。


遗憾的是在 MDN 上并没有对此有详细的说明,如果各位看官有兴趣可以翻阅一下 ECMAScript 对此的定义,了解一下最后会是哪一个结果。


NO.8

更多链接


  1. Timothy Gu, How to Read the ECMAScript Specification, https://timothygu.me/es-howto

  2. TC39, ECMAScript® 2020 Language Specification - Draft, March 10, 2020, https://tc39.es/ecma262

  3. Marja Hölttä, Understanding the ECMAScript spec, part 1, https://v8.dev/blog/understanding-ecmascript-part-1

  4. Marja Hölttä, Understanding the ECMAScript spec, part 2, https://v8.dev/blog/understanding-ecmascript-part-2

公众号:前端食堂


掘金:童欧巴


知乎:童欧巴


这是一个终身学习的男人,他在坚持自己热爱的事情,欢迎加入前端食堂,和这个男人一起开心的变胖~


推荐阅读:

分治、动态规划、回溯、贪心一锅炖

为什么 4G/5G 的直播延时依然很高


    在看和转发是莫大鼓励❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存