JavaScript 数据类型和数据结构
编程语言都内置了数据结构,但这些数据结构通常因语言而异。本文试图列出 JavaScript 中可用的内置数据结构及其特性。这些数据结构可以用来构建其他数据结构。
语言概览提供了类似的常见数据类型摘要,但包含更多与其他语言的比较。
动态和弱类型
JavaScript 是一种动态语言,具有动态类型。JavaScript 中的变量不直接与任何特定的值类型相关联,任何变量都可以被赋值(和重新赋值)为所有类型的值。
let foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean
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
隐式强制转换非常方便,但当转换发生在意想不到的地方,或者预期向另一个方向转换时(例如,字符串转换为数字而不是数字转换为字符串),可能会产生难以察觉的错误。对于符号和BigInt,JavaScript 有意禁止了某些隐式类型转换。
原始值
除对象之外的所有类型都定义了在语言最低层直接表示的不可变值。我们将这些类型的值称为_原始值_。
除null
之外的所有原始类型都可以通过typeof
运算符进行测试。typeof null
返回"object"
,因此必须使用=== null
来测试null
。
除null
和undefined
之外的所有原始类型都有其对应的对象包装器类型,这些类型提供了用于处理原始值的有用方法。例如,Number
对象提供了诸如toExponential()
之类的方法。当访问原始值上的属性时,JavaScript 会自动将该值包装成相应的包装器对象,并访问该对象上的属性。然而,访问null
或undefined
上的属性会抛出TypeError
异常,这使得引入可选链运算符成为必要。
类型 | typeof 返回值 |
对象包装器 |
---|---|---|
Null | "object" |
N/A |
Undefined | "undefined" |
N/A |
Boolean | "boolean" |
Boolean |
Number | "number" |
Number |
BigInt | "bigint" |
BigInt |
String | "string" |
String |
符号 | "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
类型表示一个逻辑实体,包含两个值:true
和false
。
数字类型
Number
类型是双精度 64 位二进制格式 IEEE 754 值。它能够存储 2-1074 (Number.MIN_VALUE
) 到 21023 × (2 - 2-52) (Number.MAX_VALUE
) 之间的正浮点数,以及相同量级的负浮点数,但它只能安全地存储 -(253 − 1) (Number.MIN_SAFE_INTEGER
) 到 253 − 1 (Number.MAX_SAFE_INTEGER
) 范围内的整数。超出此范围,JavaScript 将无法安全地表示整数;它们将被双精度浮点近似值表示。您可以使用Number.isSafeInteger()
检查数字是否在安全整数范围内。
超出可表示范围的值会自动转换。
- 大于
Number.MAX_VALUE
的正值被转换为Infinity
。 - 小于
Number.MIN_VALUE
的正值被转换为0
。 - 小于 -
Number.MAX_VALUE
的负值被转换为-Infinity
。 - 大于 -
Number.MIN_VALUE
的负值被转换为-0
。
Infinity
和-Infinity
的行为类似于数学上的无穷大,但有一些细微的差别;详见Number.POSITIVE_INFINITY
和Number.NEGATIVE_INFINITY
。
Number 类型只有一个值具有多种表示形式:0
既表示为-0
也表示为+0
(其中0
是+0
的别名)。实际上,不同表示形式之间几乎没有区别;例如,+0 === -0
为true
。但是,当您除以零时,您能够注意到这一点。
console.log(42 / +0); // Infinity
console.log(42 / -0); // -Infinity
NaN
(“Not a Number”)是一种特殊类型的数值,通常在算术运算结果无法表示为数字时遇到。它也是 JavaScript 中唯一一个不等于自身的值。
尽管数字在概念上是“数学值”并总是隐式地以浮点编码,但 JavaScript 提供了位运算符。在应用位运算符时,数字首先会转换为 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 值并不总是比数字更精确,也不总是比数字不精确,因为 BigInt 不能表示小数,但可以更精确地表示大整数。两种类型都不包含另一种,它们也不能相互替代。如果 BigInt 值与普通数字混合在算术表达式中,或者它们被隐式转换,则会抛出TypeError
。
字符串类型
String
类型表示文本数据,编码为一系列 16 位无符号整数值,代表UTF-16 码元。字符串中的每个元素都占据字符串中的一个位置。第一个元素位于索引0
,下一个位于索引1
,依此类推。字符串的长度是其中的 UTF-16 码元数,这可能与 Unicode 字符的实际数量不对应;有关更多详细信息,请参阅String
参考页面。
JavaScript 字符串是不可变的。这意味着一旦字符串创建,就无法修改它。字符串方法会根据当前字符串的内容创建新字符串——例如:
- 使用
substring()
获取原始字符串的子字符串。 - 使用连接运算符 (
+
) 或concat()
连接两个字符串。
警惕代码的“字符串化类型”!
使用字符串来表示复杂数据可能会很诱人。这样做有短期好处:
- 使用连接可以轻松构建复杂字符串。
- 字符串易于调试(你看到打印出来的内容总是字符串中的内容)。
- 字符串是许多 API 的共同点(输入字段、本地存储值、使用
Response.text()
时的fetch()
响应等),并且只使用字符串可能会很诱人。
通过约定,可以在字符串中表示任何数据结构。但这并不是一个好主意。例如,使用分隔符可以模拟列表(而 JavaScript 数组更合适)。不幸的是,当分隔符用于其中一个“列表”元素时,列表就会被破坏。可以选择转义字符等。所有这些都需要约定,并增加了不必要的维护负担。
使用字符串表示文本数据。在表示复杂数据时,_解析_字符串,并使用适当的抽象。
Symbol 类型
Symbol
是一个唯一且不可变的原始值,可以用作对象属性的键(见下文)。在某些编程语言中,Symbol 被称为“原子”。符号的目的是创建唯一的属性键,这些键保证不会与其他代码的键冲突。
对象
在计算机科学中,对象是内存中的一个值,可能被标识符引用。在 JavaScript 中,对象是唯一可变的值。函数实际上也是对象,并具有额外的_可调用_能力。
属性
在 JavaScript 中,对象可以看作是属性的集合。使用对象字面量语法,可以初始化一组有限的属性;然后可以添加和删除属性。对象属性等同于键值对。属性键可以是字符串或符号。当其他类型(如数字)用于索引对象时,这些值会隐式转换为字符串。属性值可以是任何类型的值,包括其他对象,这使得构建复杂数据结构成为可能。
对象属性有两种类型:_数据_属性和_访问器_属性。每个属性都有相应的_特性_。每个特性都由 JavaScript 引擎内部访问,但您可以通过Object.defineProperty()
设置它们,或通过Object.getOwnPropertyDescriptor()
读取它们。您可以在Object.defineProperty()
页面上阅读有关各种细微差别的更多信息。
数据属性
数据属性将键与值关联。它可以由以下属性描述:
value
-
通过对属性进行 get 访问检索到的值。可以是任何 JavaScript 值。
可写
-
一个布尔值,指示属性是否可以通过赋值更改。
可枚举
-
一个布尔值,指示属性是否可以通过
for...in
循环枚举。另请参阅属性的可枚举性和所有权,了解可枚举性如何与其他函数和语法交互。 可配置
-
一个布尔值,指示属性是否可以删除,是否可以更改为访问器属性,以及是否可以更改其属性。
访问器属性
将键与两个访问器函数(get
和set
)之一关联,以检索或存储值。
注意:重要的是要认识到它是访问器_属性_——而不是访问器_方法_。我们可以通过使用函数作为值来为 JavaScript 对象提供类似类的访问器——但这并不能使该对象成为一个类。
访问器属性具有以下属性:
get
-
在每次对值进行 get 访问时,都会调用一个不带参数的函数来检索属性值。另请参阅getter。可能为
undefined
。 set
-
一个函数,它带有一个包含赋值值的参数。每当尝试更改指定属性时执行。另请参阅setter。可能为
undefined
。 可枚举
-
一个布尔值,指示属性是否可以通过
for...in
循环枚举。另请参阅属性的可枚举性和所有权,了解可枚举性如何与其他函数和语法交互。 可配置
-
一个布尔值,指示属性是否可以删除,是否可以更改为数据属性,以及是否可以更改其属性。
对象的原型指向另一个对象或null
——它在概念上是对象的隐藏属性,通常表示为[[Prototype]]
。对象[[Prototype]]
的属性也可以在对象本身上访问。
对象是临时键值对,因此它们经常被用作映射。但是,可能存在人体工程学、安全性和性能问题。请使用Map
来存储任意数据。 Map
引用包含了关于普通对象和映射之间存储键值关联的优缺点更详细的讨论。
日期
JavaScript 提供了两套用于表示日期的 API:传统的Date
对象和现代的Temporal
对象。Date
有许多不良设计选择,应尽可能在新代码中避免使用。
索引集合:数组和类型化数组
数组是普通对象,其中整数键属性和length
属性之间存在特定的关系。
此外,数组继承自Array.prototype
,它提供了许多方便的方法来操作数组。例如,indexOf()
在数组中搜索值,push()
将元素附加到数组中。这使得数组成为表示有序列表的完美选择。
类型化数组提供了一个底层二进制数据缓冲区的类似数组的视图,并提供了许多与数组对应物具有相似语义的方法。“类型化数组”是多种数据结构的统称,包括Int8Array
、Float32Array
等。有关更多信息,请查看类型化数组页面。类型化数组通常与ArrayBuffer
和DataView
结合使用。
键控集合:Map、Set、WeakMap、WeakSet
这些数据结构将对象引用作为键。Set
和WeakSet
表示唯一值的集合,而Map
和WeakMap
表示键值关联的集合。
您可以自行实现Map
和Set
。然而,由于对象无法比较(例如,就<
“小于”而言),引擎也不会暴露其对象的哈希函数,因此查找性能必然是线性的。它们的本机实现(包括WeakMap
)可以具有近似对数时间到常数时间的查找性能。
通常,要将数据绑定到 DOM 节点,可以直接在对象上设置属性,或使用data-*
属性。缺点是数据可供在相同上下文中运行的任何脚本访问。Map
和WeakMap
可以轻松地将数据_私下_绑定到对象。
WeakMap
和WeakSet
只允许可垃圾回收的值作为键,这些值是对象或未注册的符号,并且即使它们仍存在于集合中,这些键也可能被回收。它们专门用于内存使用优化。
结构化数据:JSON
JSON (JavaScript Object Notation) 是一种轻量级数据交换格式,源自 JavaScript,但被许多编程语言使用。JSON 构建了可以在不同环境甚至跨语言传输的通用数据结构。有关更多详细信息,请参阅JSON
。
标准库中的更多对象
JavaScript 有一个内置对象的标准库。阅读参考文档以了解有关内置对象的更多信息。
类型转换
如上所述,JavaScript 是一种弱类型语言。这意味着您通常可以在需要另一种类型的地方使用一种类型的值,语言会自动将其转换为正确的类型。为此,JavaScript 定义了一些强制转换规则。
原始值强制转换
当需要原始值但对实际类型没有强烈偏好时,使用原始值强制转换过程。这通常发生在字符串、数字或BigInt都同样可接受时。例如:
Date()
构造函数,当它接收到一个不是Date
实例的参数时——字符串表示日期字符串,而数字表示时间戳。+
运算符——如果一个操作数是字符串,则执行字符串连接;否则,执行数字加法。==
运算符——如果一个操作数是原始值,而另一个是对象,则对象被转换为没有首选类型的原始值。
如果值已经是原始值,此操作不进行任何转换。对象通过调用其[Symbol.toPrimitive]()
(以"default"
为提示)、valueOf()
和toString()
方法,按此顺序转换为原始值。请注意,原始值转换优先调用valueOf()
,这类似于数字转换的行为,但与字符串转换不同。
如果存在,[Symbol.toPrimitive]()
方法必须返回一个原始值——返回一个对象会导致TypeError
。对于valueOf()
和toString()
,如果其中一个返回一个对象,则返回值将被忽略,并使用另一个的返回值;如果两者都不存在,或者两者都不返回原始值,则会抛出TypeError
。例如,在以下代码中:
console.log({} + []); // "[object Object]"
{}
和[]
都没有[Symbol.toPrimitive]()
方法。{}
和[]
都从Object.prototype.valueOf
继承valueOf()
,它返回对象本身。由于返回值是一个对象,它被忽略。因此,会调用toString()
。{}.toString()
返回"[object Object]"
,而[].toString()
返回""
,因此结果是它们的连接:"[object Object]"
。
在转换为任何原始类型时,[Symbol.toPrimitive]()
方法始终优先。原始转换通常行为类似于数字转换,因为valueOf()
优先调用;但是,具有自定义[Symbol.toPrimitive]()
方法的对象可以选择返回任何原始值。Date
和Symbol
对象是唯一覆盖[Symbol.toPrimitive]()
方法的内置对象。Date.prototype[Symbol.toPrimitive]()
将"default"
提示视为"string"
,而Symbol.prototype[Symbol.toPrimitive]()
忽略提示并始终返回一个符号。
数字强制转换
有两种数字类型:Number和BigInt。有时语言会特别期望一个数字或一个 BigInt(例如Array.prototype.slice()
,其中索引必须是一个数字);其他时候,它可能容忍两者并根据操作数的类型执行不同的操作。对于不允许从其他类型进行隐式转换的严格强制转换过程,请参阅数字强制转换和BigInt强制转换。
数值强制转换与数字强制转换几乎相同,不同之处在于 BigInt 会按原样返回,而不会导致TypeError
。所有算术运算符都使用数值强制转换,因为它们对数字和 BigInt 都进行了重载。唯一的例外是一元加号,它总是执行数字强制转换。
其他强制转换
除 Null、Undefined 和 Symbol 之外的所有数据类型都有其各自的强制转换过程。有关更多详细信息,请参阅字符串强制转换、布尔强制转换和对象强制转换。
正如您可能已经注意到的,对象可以通过三种不同的路径转换为原始值:
- 原始值强制转换:
[Symbol.toPrimitive]("default")
→valueOf()
→toString()
- 数值强制转换、数字强制转换、BigInt强制转换:
[Symbol.toPrimitive]("number")
→valueOf()
→toString()
- 字符串强制转换:
[Symbol.toPrimitive]("string")
→toString()
→valueOf()
在所有情况下,如果[Symbol.toPrimitive]()
存在,则必须可调用并返回一个原始值,而valueOf
或toString
如果不可调用或返回一个对象,则将被忽略。在过程结束时,如果成功,结果保证是一个原始值。然后,根据上下文,生成的原始值将接受进一步的强制转换。
另见
- Oleksii Trekhleb 的JavaScript 数据结构和算法
- Nicholas C. Zakas 的JavaScript 中的计算机科学