所有文章都在Github上,欢迎star、watch。


本篇来讲一下ES里的数据类型。

数据类型

ES里的数据类型分两种。

一种是语言类型,就是我们平常写的12,'xccurate',undefined等等类型。

一种是规范类型,是用来描述ECMAScript语言结构和ECMAScript语言类型的值,例如引用Reference,列表List等等。

先来看看基本的语言类型吧。

语言类型

ES的语言类型有七种:

  • Undefined类型:只有一个值:undefined
  • Null类型:只有一个值:null
  • Boolean类型:有两个值:truefalse
  • String类型:是零个或多个16位无符号整数值的所有有序序列的集合,最大长度为2^53 -1个元素。它的严格定义就不深究了,想了解的看🔗
  • Symbol类型:每个值都是独一无二的,例如Symbol('s1mple')。规范中定义了一些内置的Symbol值🔗
  • Number类型:一共18437736874454810627个值,包括NaN+Infinity-Infinity。详细定义看🔗
  • Object类型:例如:let a = {} 这就得好好说说了,见下方。

Object类型

对象是属性property的集合。

属性使用键值对来标识,属性的键的值可以为String类型(可以为空'')或Symbol类型。

属性分为两种:

  • 数据属性data property:将键值与一个ES语言值和一组布尔属性相关联。
  • 访问器属性accessor property:将键值与一个或两个访问器函数和一组布尔属性相关联。访问器函数用于存储或检索与属性关联的ECMAScript语言值。

所有对象都是属性的逻辑集合。但是有很多种对象在访问和操作其属性的语义上有所不同。

普通对象(Ordinary objects)是最常见的对象形式,具有默认对象语义。

怪异对象(exotic object)是其属性语义与默认语义不同的任何形式的对象。

属性的特性

特性attribute用来定义和解释对象属性的状态。

其中数据属性的特性有:[[Value]][[Writable]][[Enumerable]][[Configurable]]

访问器属性的特性有:[[Get]][[Set]][[Enumerable]][[Configurable]]

它们的意义也不多说了,随便一查都有,不是重点。

对象的内部方法与内部插槽

内部方法 Object Internal:其算法制定了对象的实际语义。不是ES语言的一部分,由规范定义,仅用于说明。内部方法是多态的,不同的对象可能会执行不同的算法。

内部插槽 Internal Slots:是给算法使用的内部状态。不是对象属性,不继承。其值可能为规范类型或者语言类型。(为了方便理解,其实可以把它看作内部属性,不对开发者可见。而且内部插槽这个名字实在太别扭了😂,下文就叫做内部属性了。)

这两个都是规范定义的用来描述ES对象行为的东西,可以简单的理解为对象的函数和属性。

内部方法和内部属性均用双方括号标识:`[[xxxxx]]`

这个表格🔗总结了对象的必要的内部方法,每个对象都必须有其算法,然而却不一定相同。

简略说一下有什么:

[[GetPrototypeOf]][[SetPrototypeOf]]:获取和设置对象的原型prototype

[[IsExtensible]]:决定是否可以向此对象添加其他属性。

[[PreventExtensions]]:决定是否可以将新属性添加到此对象。// 这个跟上面那个有什么区别还不太清楚

[[GetOwnProperty]][[DefineOwnProperty]]:获取和设置对象(名称为参数的)属性的属性描述符。

[[HasProperty]]:判断对象是否有自己的或继承的(名称为参数的)属性。

[[Get]][[Set]]:获取和设置(名称为参数的)属性的值。

[[Delete]]:删除对象(名称为参数的)属性。

[[OwnPropertyKeys]]:返回对象自身的属性的列表。

上篇说过,函数是可调用对象。所以其具有额外的内部方法:[[Call]]。而构造函数具有额外的内部方法:[[Construct]]

也就是说函数执行时最终调用的就是函数对象的[[Call]]方法,构造函数构造对象时,执行的是构造函数的[[Construct]]方法。

它们的的实际语义到时候再仔细分析。

接下来一小节🔗讲的是这些基本内部方法不变的行为。例如返回类型的规定,等等。这些规则是所有对象必须遵守的,尽管其算法有可能不同。这些边边角角不用开发者操心,就不详述了。

内在对象

内在对象 Intrinsic Objects是规范中的算法要引用到的内置对象。像ArrayStringObject及其原型等等。

详细见表🔗,就先不列举了,等到用到的时候再说。

多说一句,这组内置对象是每个领域Realm中有一组。什么是领域呢,例如一个页面中的iframe中的Array对象与该页面中的Array对象就不一样,它们就是两个领域的。

规范类型

规范类型是在规范中使用的,用来描述ES语义的、无法在规范外使用类型。

List和Record类型

List类型用来解释函数参数列表的执行。List类型的值是包含单个值的列表元素的简单有序序列。

其实可以简单理解为ES的数组,但是写规范的时候还不存在ES的数组类型,只好用List来描述像数组这样的类型。例如,«1,2»定义了一个List值,它具有两个元素,每个元素被初始化为一个特定的值。 一个新的空列表可以表示为«»。

(在上篇提到的ES解释器engine262中List实现就是用数组来实现的。)

Record类型用于描述规范算法中的数据聚合。Record类型值由一个或多个命名字段组成。 每个字段的值都是ECMAScript值或由与Record类型相关联的名称表示的抽象值。 字段名称始终用双括号括起来,例如[[value]]

这个就像是ES里的对象了,使用也很像:R.[[Field2]]是名为[[Field2]]的R的字段的缩写。

(在engine262里Record就是用对象来实现的。)

Set 和 Relation类型

Set类型用于解释内存模型中使用的无序元素集合,其中没有元素出现超过一次。元素可以添加到集合中,也可以从集合中移除。集合可以合并、交叉或从彼此中减去。

Relation类型用于解释Set上的约束。

// 这两个我还没遇到过,不太懂就不说了😂。

Completion Record 类型

Completion Record类型用来解释值和控制流的运行时传播。

ECMAScript规范中的每个运行时语义都显式或隐式返回一个报告其结果的完成Completion Record。

(这个定义读起来就很拗口,不过不是很重要,简单理解一下就行。)

Completion Record是一个Record,所以就可以用对象来描述它,它有三个字段:

1
2
3
4
5
Completion Record = {
[[type]] // Completion的类型,有 normal, break, continue, return, throw 5种类型
[[value]] // 返回的值为ES语言值或空,仅当当[[type]] 为 normal,return, throw时有值
[[target]] // 定向控制转移的目标label,为string或空,仅当[[type]] 为break, continue时有值
}

[[type]]normal时返回的Completion Record称作normal completion。其余的都称为abrupt completion

在规范的各种抽象操作(也叫算法)中会有这样的返回 Return Infinity,这就是隐式返回了一个Completion Record,相当于 Return NormalCompletion("Infinity")

规范在算法中经常有 ? Call(exoticToPrim, input, « hint »)这样以 ?开头的语句,它的意思如下:

res为执行Call(exoticToPrim, input, « hint »)抽象操作的结果。

上面说过,每个运行时语义都显示或隐式的返回一个Completion Record,所以

如果resabrupt completion,返回res

如果resCompletion Recordres = res.[[value]]

你还会看到 ! ToString(key)这样前缀为 !的语句,它的意思如下:

让res为ToString(key)

断言res不是abrupt completion

如果resCompletion Recordres.[[value]] = res

这两个不一样哦,注意看最后的赋值语句。

在阅读规范时,可暂时忽略其实际意义。

Reference类型

Reference类型用来解释诸如deletetypeof,赋值运算符,super关键字和其他语言特征等运算符的行为。

(在我看来,就是LHS,为了解决谁在哪里这样的问题)

一个Reference是解析的名称或属性绑定。Reference由三个组件组成:

1
2
3
4
5
{
BaseValue // 值为:undefined,Object,Boolean,String,Symbol,Number, Environment Record.
ReferencedName // String或Symbol值。
StrictReference // 布尔值,标识是否为严格模式
}

base值为undefined时表示无法解析该引用。

还有Super Reference, 用于表示使用super关键字表达的名称绑定。有个额外的 thisValue组件,其base值永远不会为Environment Record

举个栗子:

1
2
3
let a = {
b: 'test'
}

其中,查找b时就会得到一个Reference类型,其值为:

1
2
3
4
5
{
BaseValue: a,
ReferencedName: b,
StrictReference: false
}

规范还规定了对于它的一些抽象操作,例如GetBaseGetReferencedNameIsStrictReference等等。

简单说下吧:

  • GetBase ( V ):V是一个Reference。获取V的base组件。

  • GetReferencedName ( V ):V是一个Reference。获取V的name组件。

  • IsStrictReference ( V ):V是一个Reference。获取V的strict组件。

  • HasPrimitiveBase ( V ):V是一个Reference。用来判断V的base组件的值是否是这几个原始值Boolean,String,Symbol,Number

  • IsPropertyReference ( V ):V是一个Reference。用来判断V是否是属性Reference,即其base值为Object或者HasPrimitiveBase ( V )为true。

  • IsUnresolvableReference ( V ):V是一个Reference。用来判断是否是无法解析的Reference,即base值为undefined

  • IsSuperReference ( V ):V是一个Reference。就是判断V是不是Super Reference,即有没有thisValue 组件。

  • GetValue ( V )

    如果V不是Reference,就直接返回V

    是Reference,

    ​ 是无法解析的Reference,就抛出ReferenceError 错误。(就是平常遇到的变量未定义错误啦 Uncaught ReferenceError: a is not defined

    ​ 是属性Reference,

    ​ 如果是base原始值Boolean,String,Symbol,Number

    ​ 就让base = ToObject(base)。⭕1(以后有红圈的地方就是有批注的地方,圈起来,重点😂)

    ​ 返回 base.[[Get]](GetReferencedName(V), GetThisValue(V))

    ​ 那么Reference一定为 Environment Record

    ​ 返回 base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))

    ⭕1:这个地方就是为什么string、number不是对象却能够使用 String、Number对象上的方法的原因了。

    比如使用 'SOMEBODY'.toLocaleLowerCase()时要先进行LHS查询找到 'SOMEBODY'.toLocaleLowerCase是什么,才能执行函数。此时的Reference就是:

    1
    2
    3
    4
    5
    {
    BaseValue: 'SOMEBODY',
    ReferencedName: toLocaleLowerCase,
    StrictReference: false
    }

    判断 'SOMEBODY'是原始值,就进行ToObject,这个操作会将其变成一个String对象,再获取toLocaleLowerCase方法。

    当Reference为 Environment Record时,由于Environment Record还没讲,不说太多,大概就是获取Environment Record绑定值。

  • PutValue ( V, W )

    如果V不是Reference,抛出ReferenceError 。

    如果无法解析Reference,

    ​ 如果是严格模式,抛出ReferenceError 。

    ​ 否则,⭕2

    ​ 获取全局对象GlobalObject

    ​ 执行 Set(globalObj, GetReferencedName(V), W, false)操作,返回其结果。

    如果是属性Reference,

    ​ 如果是base原始值Boolean,String,Symbol,Number,就ToObjet

    ​ 执行 base.[[Set]](GetReferencedName(V), W, GetThisValue(V))操作,

    ​ 如果返回值为false,且为严格模式,抛出ReferenceError 。

    ​ 那么一定是Environment Record,

    ​ 执行 base.SetMutableBinding(GetReferencedName(V), W, IsStrictReference(V))。返回其结果。

    ⭕2:这个地方就是平常我们没声明变量直接进行赋值操作 a = 1时的算法,会在全局对象上新建一个属性,并赋给其值。

    当Reference为 Environment Record时,大概进行的操作就是在类型为Environment Record 的base上创建一个可变绑定,并赋值。

  • GetThisValue ( V ):当为SuperReference时返回thisValue 组件的值。

  • InitializeReferencedBinding ( V, W ):此时base为Environment Record,执行 InitializeBinding(GetReferencedName(V), W)操作,就是初始化绑定。

总算讲完了,Reference类型算是比较重要的一种规范类型了,理解ES运行时语义绕不开它,所以讲了很多,不像Completion Record类型,没讲太多,一方面对于理解语义无关紧要,一方面是我也不太懂😂。

Property Descriptor类型

属性描述符类型,用来解释对象属性的特性的操作,其值为Record类型,分为数据属性描述符和访问器属性描述符。

它的抽象操作我就不多说了,自己了解下就行了🔗

Lexical Environment 类型 Environment Record类型

这两个是大部头,按下不表,下面有一节专门介绍,主要是用来描述ES里作用域,标识符绑定等等。

额外说一句,我看到现在很多教程里都还在用活动对象(Activation Object)来解释作用域、闭包时,还纳闷怎么没在规范里看到,后来一搜才发现,AO、VO是ES3规范里的东西,打ES5后就再也没这俩词了。。

Data Blocks

用来描述字节大小(8位)数值的不同且可变的序列。暂时我也不懂,就先不说了😂。


这篇就先结束到这吧,本来打算把下章抽象操作也讲完的,一写下来发现有点长,就下篇讲吧。下篇主要是类型测试、类型转换,还有对象上的基本操作。

最后如果对大家有用的话,求关注、star,github 仓库🔗。🙏🙏