HOWTO · JavaScript

JavaScript 元组替代方案:数组、对象与 TypeScript

JavaScript 没有原生元组类型。根据数据契约选择数组、对象、Object.freeze()、解构赋值或 TypeScript 元组。

本页内容

JavaScript 在运行时没有原生元组类型。对于坐标这类简短、有固定顺序的数据,可以使用数组并通过解构赋值读取。字段名称更能说明含义时,应使用对象。需要在运行时阻止直接修改时,可以冻结数组;需要在开发阶段检查固定长度和元素类型时,则使用 TypeScript 元组。

使用数组表示有序值

当每个位置都有稳定含义时,数组可以作为类似元组的值。以下函数按最小值、最大值的顺序返回结果:

function minMax(values) {
  return [Math.min(...values), Math.max(...values)];
}

const [minimum, maximum] = minMax([7, 3, 12]);
console.log(minimum, maximum);

输出:

3 12

解构赋值为各个位置提供了清晰的局部变量名。请为调用方说明返回顺序。JavaScript 不会强制数组长度或元素类型,因此外部数据在作为位置契约使用之前必须验证。

仅在需要运行时保护时冻结数组

const 只阻止变量重新赋值,不会阻止修改变量引用的数组。Object.freeze() 冻结的是数组本身:

"use strict";

const point = Object.freeze([48.8566, 2.3522]);

console.log(point[0], point[1], Object.isFrozen(point));
// point[0] = 0; // TypeError in strict mode
// point.push(3); // TypeError

输出:

48.8566 2.3522 true

Object.freeze() 返回传入的同一个对象,并不会创建副本。如果调用方仍然拥有输入数组,应先复制再冻结,以免意外限制调用方对原数组的操作。

function asFrozenPoint(value) {
  if (
    !Array.isArray(value) ||
    value.length !== 2 ||
    !value.every(Number.isFinite)
  ) {
    throw new TypeError("Expected two finite coordinates");
  }

  return Object.freeze([...value]);
}

冻结是浅层的。它会阻止修改数组的直接元素、长度和自身属性,但数组元素中的嵌套对象仍然可以修改:

const settings = Object.freeze([{ theme: "light" }]);

settings[0].theme = "dark";
console.log(settings[0].theme);

输出:

dark

因此,不应把浅层冻结的嵌套结构称为完全不可变。深度冻结需要递归访问嵌套值并处理循环引用,只有确实需要这种保证时才值得实现。

名称比位置更清楚时使用对象

当一个值包含多个字符串、布尔值、可选字段或将来可能增加的字段时,仅靠位置很容易产生误解:

function createUser() {
  return { id: 42, name: "Ada", active: true };
}

const { id, name, active } = createUser();

数组适合 [x, y]Object.entries() 返回的 [key, value] 这类紧凑且含义明确的组合。如果调用方必须记住索引 12 的含义,带命名属性的对象通常更安全。

Object.entries() 已经提供了有文档说明的位置组合。通过解构赋值,可以在使用处明确两个位置的含义:

for (const [key, value] of Object.entries({ theme: "light" })) {
  console.log(`${key}: ${value}`);
}

输出:

theme: light

使用 TypeScript 检查元组契约

TypeScript 可以表达数组的长度、元素顺序和元素类型:

const point: readonly [number, number] = [48.8566, 2.3522];

readonly 会在 TypeScript 源码中阻止写入。编译后,该值仍是普通 JavaScript 数组,并不会自动在运行时冻结。只有同时需要静态类型检查和运行时保护时,才需要把 TypeScript 元组与 Object.freeze() 结合使用。

总结

JavaScript 数组可以按约定作为元组使用,并通过解构赋值读取。命名字段请选择对象,浅层运行时保护请选择 Object.freeze(),固定长度和类型的静态检查请选择 TypeScript 元组。