JavaScript 数据类型和数据结构

所有编程语言都具有内置的数据结构,但这些结构在不同语言之间通常有所不同。本文试图列出 JavaScript 中可用的内置数据结构及其属性。这些结构可以用来构建其他数据结构。

语言概述 提供了类似的常见数据类型摘要,但对其他语言进行了更多比较。

动态和弱类型

JavaScript 是一种动态语言,具有动态类型。JavaScript 中的变量没有直接与任何特定值类型相关联,并且任何变量都可以被赋值(和重新赋值)所有类型的变量。

js
let foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean

JavaScript 也是一种弱类型语言,这意味着它允许在操作涉及类型不匹配时进行隐式类型转换,而不是抛出类型错误。

js
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 有意地禁止了某些隐式类型转换。

原始值

除了 Object 之外的所有类型都定义了不可变的值,这些值直接在语言的最低级别表示。我们将这些类型的变量称为原始值。

除了 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 "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 类型表示逻辑实体,包含两个值:true 和 false。

布尔值通常用于条件操作,包括三元运算符、if...else、while 等。

Number 类型

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_INFINITYNumber.NEGATIVE_INFINITY

Number 类型只有一个具有多种表示的值:0 既表示为 -0 又表示为 +0(其中 0+0 的别名)。实际上,不同表示之间几乎没有区别;例如,+0 === -0true。但是,当你除以零时,你就可以注意到这一点。

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

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

尽管从概念上讲,数字是“数学值”,并且始终隐式地用浮点数编码,但 JavaScript 提供了 位运算符。应用位运算符时,数字首先被转换为 32 位整数。

注意:尽管位运算符可以用于使用 位掩码 在单个数字中表示多个布尔值,但这通常被认为是不好的做法。JavaScript 提供了其他方法来表示一组布尔值(例如布尔值的数组,或者将布尔值分配给命名属性的对象)。位掩码还会使代码更难阅读、理解和维护。

在非常受限的环境中(例如,尝试应对本地存储的限制,或者在极端情况下(例如,网络上的每一位都很重要)时),可能需要使用这些技术。仅当它是优化大小可以采取的最后措施时,才应考虑此技术。

BigInt 类型

BigInt 类型是 JavaScript 中的一种数字基本类型,它可以表示任意大小的整数。使用 BigInts,你可以安全地存储和操作大型整数,甚至超过 Numbers 的安全整数限制 (Number.MAX_SAFE_INTEGER)。

BigInt 通过在整数末尾添加 n 或调用 BigInt() 函数来创建。

此示例演示了在递增 Number.MAX_SAFE_INTEGER 时返回预期结果的情况。

js
// 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

你可以使用大多数运算符来处理 BigInts,包括 +*-**% - 唯一禁止的是 >>>。BigInt 不 严格等于 具有相同数学值的 Number,但它 松散地 这样。

BigInt 值既不总是比数字更精确,也不总是比数字更不精确,因为 BigInts 不能表示小数,但可以更准确地表示大整数。这两种类型都不包含另一种类型,并且它们不能互换使用。如果 BigInt 值与普通数字混合在算术表达式中,或者如果它们被 隐式转换 为彼此,则会抛出 TypeError

字符串类型

String 类型表示文本数据,并被编码为一系列 16 位无符号整数值,这些整数值代表 UTF-16 代码单元。字符串中的每个元素在字符串中都占据一个位置。第一个元素位于索引 0 处,下一个位于索引 1 处,依此类推。字符串的 长度 是它包含的 UTF-16 代码单元数量,这可能与 Unicode 字符的实际数量不符;有关详细信息,请参阅 String 参考页面。

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

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

小心“字符串化”你的代码!

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

  • 使用连接很容易构建复杂的字符串。
  • 字符串很容易调试(你看到打印的内容始终是字符串中的内容)。
  • 字符串是许多 API 的共同点 (输入字段本地存储 值、fetch() 响应(使用 Response.text() 时),等等),并且可能很容易只使用字符串。

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

将字符串用于文本数据。在表示复杂数据时,解析字符串,并使用适当的抽象。

符号类型

Symbol 是一个唯一不可变的基本值,可以用作 Object 属性的键(见下文)。在一些编程语言中,符号被称为“原子”。符号的目的是创建唯一的属性键,这些键保证不会与其他代码中的键发生冲突。

对象

在计算机科学中,对象是内存中的一个值,它可能被 标识符 引用。在 JavaScript 中,对象是唯一 可变 的值。函数 事实上也是对象,具有额外的可调用能力。

属性

在 JavaScript 中,对象可以看作是属性的集合。使用 对象字面量语法,初始化一组有限的属性;然后可以添加和删除属性。对象属性等效于键值对。属性键是 字符串符号。当使用其他类型(例如数字)来索引对象时,这些值会隐式转换为字符串。属性值可以是任何类型的值,包括其他对象,这使得构建复杂的数据结构成为可能。

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

数据属性

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

value

通过对属性进行获取访问检索到的值。可以是任何 JavaScript 值。

writable

一个布尔值,指示是否可以通过赋值更改属性。

enumerable

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

configurable

一个布尔值,指示属性是否可以删除,是否可以更改为访问器属性,以及是否可以更改其属性。

访问器属性

将键与两个访问器函数 (getset) 之一关联起来,以检索或存储值。

注意:重要的是要认识到它是访问器属性 - 而不是访问器方法。我们可以通过使用函数作为值来为 JavaScript 对象提供类似类的访问器 - 但这不会使对象成为类。

访问器属性具有以下属性

get

一个函数,在执行对值的获取访问时,使用空参数列表调用该函数以检索属性值。另请参阅 获取器。可以为 undefined

set

一个函数,使用包含分配值的参数进行调用。在尝试更改指定属性时执行。另请参阅 设置器。可以为 undefined

enumerable

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

configurable

一个布尔值,指示属性是否可以删除,是否可以更改为数据属性,以及是否可以更改其属性。

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

对象是临时键值对,因此它们通常用作映射。但是,可能存在可用性、安全性和性能问题。使用 Map 来存储任意数据。Map 参考 包含关于将普通对象和映射用于存储键值关联的优缺点的更详细讨论。

日期

在表示日期时,最佳选择是使用 JavaScript 中的内置 Date 实用程序。

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

数组 是普通对象,它们之间存在整数键属性和 length 属性之间的特殊关系。

此外,数组从 Array.prototype 继承,这提供了一些方便的方法来操作数组。例如,indexOf() 在数组中搜索值,而 push() 将元素追加到数组。这使得数组成为表示有序列表的理想选择。

类型化数组 提供了对底层二进制数据缓冲区的类数组视图,并提供了许多与数组对应物具有类似语义的方法。“类型化数组”是一个总称,用于一系列数据结构,包括 Int8ArrayFloat32Array 等。有关更多信息,请查看 类型化数组 页面。类型化数组通常与 ArrayBufferDataView 结合使用。

键控集合:映射、集合、弱映射、弱集合

这些数据结构以对象引用作为键。SetWeakSet 表示一组唯一的值,而 MapWeakMap 表示一组键值关联。

你可以自己实现 MapSet。但是,由于对象不能比较(例如,在 < “小于”的意义上),引擎也不会公开其用于对象的哈希函数,查找性能必然是线性的。它们的本机实现(包括 WeakMap)的查找性能大约是对数时间到常数时间。

通常,要将数据绑定到 DOM 节点,可以设置对象上的属性,或使用data-*属性。 这样做会导致数据对在同一上下文中运行的任何脚本都可用。 MapWeakMap可以轻松地将数据私有绑定到对象。

WeakMapWeakSet只允许可垃圾回收的值作为键,这些值要么是对象,要么是未注册的符号,即使键保留在集合中,它们也可能被回收。 它们专门用于内存使用优化

结构化数据:JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,源自 JavaScript,但被许多编程语言使用。 JSON 构建了可以在不同环境之间甚至跨语言传输的通用数据结构。 有关更多详细信息,请参阅JSON

标准库中的更多对象

JavaScript 具有一个内置对象的标准库。 阅读参考以了解有关内置对象的更多信息。

类型强制

如上所述,JavaScript 是一种弱类型语言。 这意味着你经常可以在需要另一种类型的地方使用一种类型的值,语言会为你将其转换为正确的类型。 为此,JavaScript 定义了一些强制规则。

原始强制

当期望一个原始值,但对实际类型没有强烈的偏好时,会使用原始强制过程。 这通常是在字符串数字BigInt 同样可接受时。 例如

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

如果该值已经是原始值,此操作不会进行任何转换。 通过按顺序调用其[Symbol.toPrimitive]()(以"default"作为提示)、valueOf()toString()方法将对象转换为原始值。 注意,原始转换在toString()之前调用valueOf(),这类似于数字强制的行为,但不同于字符串强制

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

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

{}[]都没有[Symbol.toPrimitive]()方法。 {}[]都从Object.prototype.valueOf继承valueOf(),它返回对象本身。 由于返回值是一个对象,它会被忽略。 因此,toString()会被调用。 {}.toString()返回"[object Object]",而[].toString()返回"",所以结果是它们的连接:"[object Object]"

[Symbol.toPrimitive]()方法在进行转换为任何原始类型时始终优先。 原始转换通常表现得像数字转换,因为valueOf()优先调用;但是,具有自定义[Symbol.toPrimitive]()方法的对象可以选择返回任何原始值。 DateSymbol对象是唯一覆盖[Symbol.toPrimitive]()方法的内置对象。 Date.prototype[Symbol.toPrimitive]()"default"提示视为"string",而Symbol.prototype[Symbol.toPrimitive]()会忽略提示并始终返回一个符号。

数字强制

有两种数字类型:NumberBigInt。 有时语言专门期望一个数字或一个 BigInt(例如Array.prototype.slice(),其中索引必须是数字);其他时候,它可能会容忍两者,并根据操作数的类型执行不同的操作。 对于不允许从另一种类型隐式转换的严格强制过程,请参阅数字强制BigInt 强制

数字强制几乎与数字强制相同,不同的是 BigInt 会按原样返回,而不是导致TypeError。 数字强制被所有算术运算符使用,因为它们对数字和 BigInt 都进行了重载。 唯一的例外是一元加号,它始终执行数字强制。

其他强制

除了 Null、Undefined 和 Symbol 之外,所有数据类型都有各自的强制过程。 有关更多详细信息,请参阅字符串强制布尔强制对象强制

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

在所有情况下,[Symbol.toPrimitive]()(如果存在)必须是可调用的并返回一个原始值,而如果valueOftoString不可调用或返回一个对象,则会忽略它们。 在过程结束时,如果成功,则结果保证是一个原始值。 然后,根据上下文,生成的原始值将接受进一步的强制。

另请参见