【第1877期】理解ECMAScript规范(一)
前言
今日早读文章由「深入理解 TypeScript」主要译者@前端三毛翻译授权分享。
正文从这开始~~
在这篇文章中,为我们将会使用规范中的一个小的功能来做为切入点,从中去尝试理解一些特殊操作符,让我们开始吧!
即使你了解 JavaScript,阅读它的规范(ECMAScript 语言规范简称 ECMAScript 规范[1]) 也是会令人生畏的。至少这是我第一次阅读时的感受。
让我们通过一个具体的示例开始。下面的代码演示了 Object.prototype.hasOwnProperty
的使用:
const o = { foo: 1};
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
在这个例子中, o
没有 hasOwnProperty 属性,因此我们会在它的原型链上寻找 hasOwnProperty
属性,最终我们在 Object.prototype
, o
的原型上找到了它。
为了描述 Object.prototype.hasOwnProperty
是怎么工作的,ECMAScript 规范使用了伪代码来描述它:
Object.prototype.hasOwnProperty(v)[2]
当带参数 v 调用 hasOwnProperty,会经过以下步骤
令 P 为 ?ToPropertyKey(V)
令 O 为 ?ToObject(thisvalue)
返回 ?HasOwnProperty(O,P)
以及:
HasOwnProperty[3]抽象操作符, 它用来确认一个对象自身是否含有某个特定键的属性,返回有一个布尔值。调用这个操作符需要两个参数, O
和 P
,前者是一个对象,后者是个属性键的名称。这个抽象操作符的执行,会经过以下步骤:
1、断言:Type(O)
是一个对象
2、断言:IsPropertyKey(P)
为 true
3、令 desc 为 ?O.[[GetOwnProperty]](P)
4、如果 desc 是 undefined,返回 false
5、返回 true
那么什么是一个「抽象操作符」?[[]]
里面的内容是什么?为什么在一个函数之前有一个 ?
,这个「推断」又是什么意思呢?
现在,让我们找到答案!
语言类型和规范类型
让我们先来看一对相似的概念,语言类型和规范类型。ECMAScript 规范使用 undefined
、 true
、 false
,等值 —— 我们已从 JavaScript 了解它们。它们都是「语言值[4]」,也就是规范中定义的语言类型的值。
在规范中也使用了语言内置的值,比如一个内置的数据类型可能包含一个值为 true
和 false
的字段,相反,JavaScript 引擎通常不会使用这些内置的语言值。例如,如果 JavScript 引擎是使用 C++ 编写,它可能会使用 c++ 里面的 true
和 false
,而不是 JavaScript 内部的 true
和 false
。
除了语言类型外,规范中还使用了规范类型[5],它们是仅仅出现在规范中而没有出现在 JavaScript 中的类型。JavaScript 引擎不必(当然也可以)实现它们。在这篇文章中,我们将会去了解其中一个规范类型 -- Record 以及它的子类型 Completion Record。
抽象操作符
抽象操作符[6]是规范中定义的函数,定义它们是为了更加简洁的书写规范。JavaScript 引擎也不必把它们做为一个单独的内部函数来实现它们。在 JavaScript 中,它们不能被调用。
内部的插槽和函数
为了更好的书写规范,内部的插槽和函数[7]被定义,它们通常被包裹在 [[]]
中。
内部插槽是 JavaScript 对象或者是规范类型中的数据成员,它们用来存储对象的状态。内部的方法是 JavaScript 对象方法中的成员。
例如每个 JavaScript 对象都有一个内部插槽 [[Prototype]]
以及内部方法 [[GetOwnProperty]]
。
内部插槽和内部方法在 JavaScript 不可被访问,例如你不能访问 o.[[Prototype]]
或者调用 o.[[GetOwnProperty]]()
,JavaScript 引擎可以实现它们供自己内部使用,但这不是必须的。
有些时候,内部的方法功能将会委托给一个名字相似的抽象操作符,比如普通对象中的 [[GetOwnProperty]]
:
[[GetOwnProperty]](p)
当带有参数 P
调用 O
内部的方法 [[GetOwnProperty]]
时,会进行以下步骤:
1、返回 !OrdinaryGetOwnProperty(O,P)
(将会在下一章节解释 ! )
OrdinaryGetOwnProperty
并不是一个内部的方法,因为它与任何对象都毫无联系,取而代之,这个对象做为参数被传入这个操作符。
OrdinaryGetOwnProperty
被称之为普通的(ordinary),因为它操作普通的对象。ECMAScript 对象可以是普通对象,也可以是变异的对象(exotic),普通对象必须拥有一些默认行为,比如一组内部方法。如果一个对象偏离这些默认行为,它就是变异的。
最广为人知变异对象是 Array,因为它的长度属性以非默认方式运行 —— 设置长度属性,能够从数组中移除一个元素。
基础的内部方法列表可以从这[8]看到。
Completion records
那么 ? 和 ! 是什么意思呢?为了理解它们,我们必须来看看 Completionrecords
[9]。
Completion Record 是一个规范中的类型(仅仅定义在规范中),JavaScript 引擎并不需要一个相应的内置数据类型。
Completion Record :
[[Type]]
:值可以是nomal
、break
、continue
、return
和throw
,除了nomal
,其他都是中断完成[[Value]]
:完成时,产生的值。比如函数的返回值,或者是一个异常(如果抛出)[[Target]]
:用来定向转移的标签
每个抽象操作符隐式返回一个 Completion Record,尽管它可能仅仅是返回了一个 Boolean 类型,它也会被 Completion Record 包裹着(详情可以查看 隐式的 Completion Values[10])。
注意点1:规范在这方面并不完全一致。因为存在一些直接返回裸露值的辅助函数,这些函数返回值按照原样使用,而不是从 Completion Record 中提取出值。通常可以从上下文中清楚看出。
注意点2: 规范的编写者正在找寻一种能让 Completion Record 更加明确的方式。
如果一个程序抛出异常,这意味着需要返回一个有特定 [[type]]
的 Completion Record,它的 [[Value]]
是一个含有错误信息的对象。到现在为止,我们将会忽略 break
、 continue
、 return
的 [[type]]
。
ReturnIfAbrupt(argument)[11] 将会进行以下步骤:
1、如果 argument
出错,返回 argument
2、把 argument
赋值给 argument.[[value]]
这就是我们期望的一个 Completion Record。如果一个中断完成了,便立即返回。否则我们将会从 Completion Record 中取值。
ReturnIfAbrupt
看起来像是一个函数的调用,但实际上并不是。它导致返回 ReturnIfAbrupt()
的函数返回,而不是 ReturnIfAbrupt
函数本身返回。它的行为更像是 C 语言中的宏(macro)。
ReturnIfAbrupt
能像下面这样使用:
1、令 obj
为 Foo()
( obj
为 Completion Record)
2、 ReturnIfAbrupt(obj)
3、 Bar(obj)
(如果进行到这步。obj
从 Completion Record 中提取的值)
因此 ? 的作用既是:?Foo()
等价于 ReturnIfAbrupt(Foo())
。使用简写的形式是有用的,我们不必每次都写捕获错误的代码。
与此相似, !Foo()
等价于:
1、令 val
为 Foo
2、断言, val
不是一个突然的中断
3、把 val
赋值给 val.[[value]]
使用这些知识,我们可以写出 Object.prototype.hasOwnProperty
:
Object.prototype.hasOwnProperty(P)
1、令 P
为 ToPropertyKey(V)
2、如果 P
是一个中断完成,返回 P
3、令 p.[[value]]
等于 p
4、令 O
为 ToObject(O)
5、如果 O
是一个中断完成,返回 O
6、令 O.[[value]]
等于 O
7、令 temp
为 HasOwnProperty(O,P)
8、如果 temp
是中断完成,返回 temp
9、令 temp
为 temp.[[value]]
10、返回 NormalCompletion
与此相似,我们也可以重写出 HasOwnProperty(O,P)
HasOwnProperty(O,P)
1、断言:Type(O)
是一个对象
2、断言:IsPropertyKey(P)
是 true
3、令 desc
为 O.[[GetOwnProperty]](P)
4、如果 desc
是一个中断完成,返回 O
5、令 desc.[[value]]
等于 desc
6、如果 desc
是 undefined
,返回 NormalCompletion(false)
7、返回 NormalCompletion(true)
我们也可以重写不带 ! 的内部方法 [[GetOwnProperty]]
:
O.[[GetOwnProperty]]
1、令 temp
为 OrdinaryGetOwnProperty(O,P)
2、断言:temp
不会是一个中断完成
3、令 temp.[[value]]
等于 temp
4、返回 NormalCompletion(temp)
在这里,我们假设 temp
是一个全新的临时变量,不会与其他任何冲突。
我们还使用了以下知识点:当 return
语句返回除 CompletionRecord
以外的其他内容时,它隐式包装在 NormalCompletion
中。
总结
到现在,我们已经了解了阅读如 Object.prototype.hasOwnProperty
以及抽象操作符 HasOwnProperty 规范所需的知识点。它们仍然需要委托其他抽象操作符去工作,基于此博客,我们应该能了解到它们是如何工作的。接下来,我们将会遇见属性描述符,它们仅仅另外一种规范类型。
参考资料
[1]ECMAScript 语言规范简称 ECMAScript 规范: https://tc39.es/ecma262/ [2]Object.prototype.hasOwnProperty(v): https://tc39.es/ecma262#sec-object.prototype.hasownproperty [3]HasOwnProperty: https://tc39.es/ecma262/#sec-hasownproperty [4]语言值: https://tc39.es/ecma262/#sec-ecmascript-language-types [5]规范类型: https://tc39.es/ecma262/#sec-ecmascript-specification-types [6]抽象操作符: https://tc39.es/ecma262/#sec-abstract-operations [7]内部的插槽和函数: https://tc39.es/ecma262/#sec-object-internal-methods-and-internal-slots [8]这: https://tc39.es/ecma262/#table-5 [9]Completion records: https://tc39.es/ecma262/#sec-completion-record-specification-type [10]Completion Values: https://v8.dev/blog/understanding-ecmascript-part-1 [11]ReturnIfAbrupt(argument): https://tc39.es/ecma262/#sec-returnifabrupt
关于本文 译者:@三毛 译文:https://mp.weixin.qq.com/s/HFb9MohJWp4fZnxYiS1emw 作者:@Marja Hölttä 原文:https://v8.dev/blog/understanding-ecmascript-part-1
@前端三毛曾分享过
为你推荐
【第1660期】 Diff ECMAScript® 2019