JavaScript 数据类型和数据结构

编程语言都具有内建的数据结构,但各种编程语言的数据结构常有不同之处。本文试图列出 JavaScript 语言中内建的数据结构及其属性,它们可以用来构建其它的数据结构。同时尽可能地描述与其它语言的不同之处。

JavaScript 语言概述提供了对常见数据类型的类似总结,但是和其它的语言有着更多的比较。

动态和弱类型

JavaScript 是一种有着动态类型动态语言。JavaScript 中的变量与任何特定值类型没有任何关联,并且任何变量都可以分配(重新分配)所有类型的值:

let foo = 42; // foo 现在是一个数值
foo = "bar"; // foo 现在是一个字符串
foo = true; // foo 现在是一个布尔值

JavaScript 也是一个弱类型语言,这意味着当操作涉及不匹配的类型是否,它将允许隐式类型转换,而不是抛出一个错误。

const foo = 42; // foo is a number
const result = foo + "1"; // JavaScript coerces foo to a string, so it can be concatenated with the other operand
console.log(result); // 421

强制隐式转换是非常方便的,但是如果开发者不打算转换,或者打算向另一个方向转换(例如,字符串转数值而不是数值到字符串),则会存在潜在的隐患。对于 symbolBigInt,JavaScript 总是不允许某些隐式类型转换。

原始值

除了 Object 以外,所有类型都定义了表示在语言最低层面的不可变 (en-US)值。我们将这些值称为原始值

除了 null,所有原始类型都可以使用 typeof 运算符测试。typeof null 返回 "object",因此必须使用 === null 来测试 null

除了 nullundefined,所有原始类型都有它们相应的对象包装类型,这为处理原始值提供可用的方法。例如,Number 对象提供向 toExponential() 这样的方法。当在原始值上访问属性时,JavaScript 会自动将值包装到相应的包装对象中,并访问对象上的属性。然而,在 nullundefined 上访问属性时,会抛出 TypeError 异常,这需要采用可选链运算符。

类型 typeof 返回值 对象包装器
Null "object" N/A
Undefined "undefined" N/A
Boolean "boolean" Boolean
Number "number" Number
BigInt "bigint" BigInt
String "string" String
Symbol "symbol" Symbol

对象包装器类的参考页面包含关于每个类型可用方法和属性类型的更多用法,以及原始类型本身的详细描述。

Null 类型

Null 类型只有一个值:null

undefined 类型

Undefined 类型只有一个值:undefined

从概念上讲,undefined 表示没有任何null 表示没有任何对象(这也可以构成 typeof null === "object" 的接口)。当某些东西没有值时,该语言通常默认为 undefined

  • 没有值(return;)的 return 语句,隐式返回 undefined
  • 访问不存在的对象属性(obj.iDontExist),返回 undefined
  • 变量声明时没有初始化(let x;),隐式初始化为 undefined
  • 许多如 Array.prototype.find()Map.prototype.get() 的方法,当没有发现元素时,返回 undefined

null 在核心语言中使用频率少得多。最重要的地方是原型链的末端——其次是与原型交互的方法,如 Object.getPrototypeOf()Object.create() 等,接受或返回 null 而不是 undefined

null 是一个关键字,但是 undefined 是一个普通的标识符,恰好是一个全局属性。在实践中,这两个差异很小,因为 undefined 不应该被重新定义或者遮蔽。

Boolean 类型

Boolean 类型表示一个逻辑实体并且包括两个值:truefalse

布尔值通常用于条件运算,包括三元运算符if...elsewhile 等。

Number 类型

Number 类型是一种基于 IEEE 754 标准的双精度 64 位二进制格式的值。它能够存储 2-1074Number.MIN_VALUE)和 21024Number.MAX_VALUE)之间的正浮点数,以及 -2-1074 和 -21024 之间的负浮点数,但是它仅能安全地存储在 -(253 − 1)(Number.MIN_SAFE_INTEGER)到 253 − 1(Number.MAX_SAFE_INTEGER)范围内的整数。超出这个范围,JavaScript 将不能安全地表示整数;相反,它们将由双精度浮点近似表示。你可以使用 Number.isSafeInteger() 检查一个数是否在安全的整数范围内。

±(2-1074 到 21024) 范围之外的值会自动转换:

+Infinity-Infinity 行为类似于数学上的无穷大,但是有一些细微的区别;更多细节,参见 Number.POSITIVE_INFINITYNumber.NEGATIVE_INFINITY

Number 类型仅有一个具有多个表现形式的值:0 同时表示为 -0+0(其中 0+0 的别名)。实际上,这两者之间机会没有区别;例如,+0 === -0true。然而,当你除以 0 的时候,你要注意到这一点:

console.log(42 / +0); // Infinity
console.log(42 / -0); // -Infinity

NaN(“Not a Number”)是一个特殊种类的数值,当算术运算的结果不表示数值时,通常会遇到它。它也是 JavaScript 中唯一不等于自身的值。

虽然 number 在概念上是一个“数学的值”,并且总是隐式的编码为浮点类型,但是 JavaScript 提供了 按位运算符。当应用按位运算符时,number 首先转换为 32 位整数。

备注: 尽管按位运算符可以使用位掩码来表示单个数值中的几个布尔值,但通常这不是一个好的做法。JavaScript 提供了表示一组布尔值的其它方法(如布尔数组,或将布尔值分配给命名属性的对象)。位掩码也往往会使代码更难读取、理解和维护。

可能有必要在非常受限的环境中使用此类技术,例如在试图应对本地存储的限制时,或在极端情况下(例如当网络上的每个位计数时)。只有当这项技术是优化尺寸的最后一项措施时,才应考虑这项技术。

BigInt 类型

BigInt 类型在 Javascript 中是一个数字的原始值,它可以表示任意大小的整数。使用 BigInt,你可以安全地存储和操作巨大的整数,甚至超过 Number 的安全整数限制(Number.MAX_SAFE_INTEGER)。

BigInt 是通过将 n 附加到整数末尾或调用 BigInt() 函数来创建的。

此示例演示了增加 Number.MAX_SAFE_INTEGER 返回预期结果的位置:

// BigInt
const x = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
x + 1n === x + 2n; // false because 9007199254740992n and 9007199254740993n are unequal

// Number
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true because both are 9007199254740992

你可以使用大多数运算符为 BigInt工作,包括 +*-**%——唯一被禁止的是 >>>。BigInt 并不是严格等于有着相同数学值的 Number,但是它是宽松的

BigInt 值并不总是更精确的,也不总是比 number 精确,因为 BigInt 不能表示分数,但是可以表示更准确的大整数。这两种类型都包含各自的类型,并且它们不能相互替代。如果 BigInt 值在算术表达式中与正则数值混合,或者它们相互隐式转换,则抛出 TypeError

String 类型

String 类型表示文本数据并编码为 UTF-16 代码单位的 16 位无符号整数值序列。字符串中的每个元素在字符串中占据一个位置。第一个元素的索引为 0,下一个是索引 1,依此类推。字符串的长度是它的元素的数量。字符串的长度是其中的 UTF-16 代码单元的数量,这可能与 Unicode 字符的实际数量不符;更多细节参见 String 参考页面。

JavaScript 字符串是不可变的。这意味着一旦字符串被创建,就不可能修改它。字符串方法基于当前字符串的内容创建一个新的字符串——例如:

  • 使用 substring() 获取原始的子字符串。
  • 使用串联运算符(+)或 concat() 将两个字符串串联。

注意代码中的“字符串类型”!

使用字符串来表示复杂的数据可能很诱人。这样做会带来短期的好处:

  • 容易通过字符串拼接来构造复杂的字串符。
  • 字符串容易被调试(你看到的打印的内容始终是字符串)。
  • 字符串通常是许多 API 的常见标准(input 字段local storage 中的值,以及 XMLHttpRequest 使用 responseText 等作为响应)而且这些 API 可能只能与字符串一同使用。

使用约定,字符串一般可以用来表示任何数据结构。但这并不总是一个好主意。例如,使用一个分隔符,可以模拟一个列表(而 JavaScript 数组可能更适合)。不幸的是,当分隔符用于列表中的元素时,列表就会被破坏。这时可以选择转义字符,等等。所有这些都需要约定,并造成不必要的维护负担。

表示文本数据时候推荐使用字符串。当需要表示复杂的数据时,使用字符串解析并使用适当的抽象。

Symbol 类型

Symbol唯一并且不可变的原始值并且可以用来作为对象属性的键(如下)。在某些程序语言当中,Symbol 也被称作“原子类型”(atom)。symbol 的目的是去创建一个唯一属性键,保证不会与其它代码中的键产生冲突。

Object

在计算机科学中,对象(object)是指内存中的可以被标识符引用的一块区域。在 JavaScript 中,对象是唯一可变 (en-US)的值。事实上,函数也是具有额外可调用能力的对象

属性

在 JavaScript 中,对象可以被看作是一组属性的集合。用对象字面量语法来定义一个对象时,会自动初始化一组有限的属性;然后,这些属性还可以被添加和移除。对象属性等价于键值对。属性键要么是字符串类型,要么是 symbol。属性值可以是任何类型的值,包括其它对象,从而可以构建复杂的数据结构。

有两种对象属性的类型:数据属性访问器属性。每个属性都有对应的特性(attribute)。JavaScript 引擎在内部内置了访问性,但是你可以通过 Object.defineProperty() 设置它们,或者通过 Object.getOwnPropertyDescriptor() 读取它们。你可以在 Object.defineProperty() 页面上读取更多有关信息。

数据属性

数据属性将键与值相关联。它可以通过以下属性来描述:

value

通过属性访问器获取值。可以是任意的 JavaScript 值。

writable

一个布尔值,表示是否可以通过赋值来改变属性。

enumerable

一个布尔值,表示是否可以通过 for...in 循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其它函数和语法交互。

configurable

一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。

访问器属性

将键与两个访问器函数(getset)像关联,以获取或者存储值。

备注: 重要的是,意识到它是访问器属性——而不是访问器方法。我们可以将函数作为值来提供给 JavaScript 对象的访问器,使得对象表现得像一个类——但这并不能使该对象成为类。

一个访问器属性有着以下的特性:

get

该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值。参见 getter。可能是 undefined

set

使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。参见 setter。可能是 undefined

enumerable

一个布尔值,表示是否可以通过 for...in 循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其它函数和语法交互。

configurable

一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。

对象的原型(prototype)指向另一个对象或者 null——从概念上讲,它是对象的隐藏属性,通常表示为 [[Prototype]]。对象的 [[Prototype]] 属性也可以在对象自身访问。

对象是临时的键值对,因此它们通常当作映射使用。然而,这可能涉及人类工程学、安全以及性能的问题。然而,可以使用 Map 来存储任意的数据。Map 引用更详细地讨论了使用普通对象和使用 map 存储键值之间的利弊。

Date

当表示日期时,最好的选择是使用在 JavaScript 内置的 Date 工具类。

索引类集合:数组和类型化数组

数组是一种以整数为键(integer-keyed)的属性并与长度(length)属性关联的常规对象。

此外,数组对象还继承了 Array.prototype 的一些操作数组的便捷方法。例如,indexOf()(搜索数组中的一个值)或 push()(向数组中添加一个元素),等等。这使得数组是表示列表或集合的最优选择。

类型化数组表示底层二进制缓冲区的类数组视图,并且提供了与数组相对应的类似语义的方法。“类型化数组”是一系列数据结构的总话术语,包括 Int8ArrayFloat32Array 等等。获取更多细节,请查看类型化数组页。类型化数组通常与 ArrayBufferDataView 一起使用。

带键的集合:Map、Set、WeakMap、WeakSet

这些数据结构把对象的引用当作键。SetWeakSet 表示唯一值的集合,而 MapWeakMap 表示键值相关联的集合。

你也可以自己实现 MapSet。然而,因为对象不能被比较(例如,在 <“小于”的意义上),另一方面,引擎也没有暴露出它的哈希函数,因此查找性能必定是线性的。它们的原始实现(包括 WeakMap)的查找性能大致为对数到相对恒定的时间。

通常,可以通过直接在对象上设置属性或使用 data-* 属性,将绑定数据到 DOM 节点。然而缺陷是在任何的脚本内,数据都运行在同样的上下文中。MapWeakMap 能够方便地将数据私密地绑定到一个对象。

WeakMapWeakSet 仅允许对象键,即使这些键留在集合中,页允许这些键被垃圾回收。它们专门用于内存使用优化

结构化数据:JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,来源于 JavaScript,同时也被多种语言所使用。JSON 构建了通用数据结构,可以在不同环境之间传输,甚至可以跨语言传输。更多细节,请参见 JSON

标准库中的更多对象

JavaScript 有一个内置对象的标准库。发现更多关于内置对象,请阅读参考

强制类型转换

如上所述,JavaScript 是一个弱类型语言。这意味值你可以经常使用一种类型的值,而另一种类型是预期的,并且该语言将为你转换它为正确的类型。为此,JavaScript 定义了少数强制规则。

强制原始值转换

在期望原始值的地方使用原始强制过程,但对实际类型应该是什么没有强烈的偏好。 在期望原始值的地方使用强制原始值转换的过程,但对实际的类型并不是什么特殊的要求。这通常是当 字符串数值BigInt 相同可以接受的时候。

  • Date() 构造函数,当它收到一个不是 Date 实例的参数时——字符串表示日期字符串,而数值表示时间戳。
  • + 运算符——如果运算对象是字符串,执行字符串串联;否则,执行数值相加。
  • == 运算符——如果一个运算对象是原始值,而另一个运算对象是对象(object),则该对象将转换为没有首选类型的原始值。

如果值已经是原始值,则此操作不会进行任何转换。对象按以下顺序调用它的 [@@toPrimitive]()(将 hint 作为 default)、valueOf()toString() 方法,将其转换为原始值。注意,原始值转换会在 toString() 方法之前调用 valueOf() 方法,这与强制数字类型转换的行为相似,但与强制字符串类型转换不同。

[@@toPrimitive]() 方法,如果存在,则必须返回原始值——返回对象,会导致 TypeError。对于 valueOf()toString(),如果其中一个返回对象,则忽略其返回值,从而使用另一个的返回值;如果两者都不存在,或者两者都返回一个原始值,则抛出 TypeError。例如,以下代码:

console.log({} + []); // "[object Object]"

{}[] 都没有 [@@toPrimitive]() 方法。{}[] 都从 Object.prototype.valueOf 继承 valueOf(),其返回对象自身。因为返回值是一个对象,因此它被忽略。因此,调用 toString() 方法。{}.toString() 返回 "[object Object]",而 [].toString() 返回 "",因此这个结果是它们的串联:"[object Object]"

在强制转换为任意的原始类型时,[@@toPrimitive]() 方法总是优先调用。原始值的强制转换的行为通常与强制 number 类型类似,因为优先调用 valueOf();然而,有着自定义 [@@toPrimitive]() 方法的对象可以选择返回任意的原始值。DateSymbol 对象是唯一重写 [@@toPrimitive]() 方法的对象。Date.prototype[@@toPrimitive]()"default" hint 视为 "string",而 Symbol.prototype[@@toPrimitive]() 忽略 hint 并始终返回一个 symbol。

强制数字类型转换

有两种数字类型:numberBigInt。有时候,该语言尤其希望是 number 或 BigInt(例如 Array.prototype.slice(),其中索引必须是一个数字);其它时候,它可能容忍并且根据运算对象的类型不同执行不同的运算。有关不允许从其它类型隐式转换的严格强制转换过程,请参阅强制 number 转换强制 BigInt 转换

强制数字类型转换与强制 number 类型转换几乎相同,只是 BigInt 会按原样返回,而不是引起 TypeError。强制数字类型转换用于所有算术运算,因为它们重载了 number 和 BigInt 类型。唯一例外的是一元加,它总是强制 number 类型转换。

强制其它类型转换

所有的数据类型,除了 Null、Undefined 以及 Symbol,都有它们各自的强制过程。更多细节,请参见强制字符串转换强制布尔值转换以及强制对象转换

你可能已经注意到,有三种不同的路径可以将对象转换为原始值:

在所有情况下,[@@toPrimitive]() 如果存在,必须可调用并返回原始值,而如果它们不可调用或返回对象,valueOftoString 将被忽略。在过程结束时,如果成功,结果保证是原始值。然后,由此产生的原始值会进一步强制类型转换,具体取决于上下文。

参见