PhysChen.com
主页
物理
笔记 科普 研究
教学
IB 课程
编程
笔记 项目
随笔
所感 所思
摄影
Shenzhen Portrait Cats Others Wuhan Japan
关于
主页
物理
笔记 科普 研究
编程
笔记 项目
摄影
Shenzhen Portrait Cats Others Wuhan Japan
教学
IB 课程
随笔
所感 所思
关于
文章目录
    JavaScript 5. 对象、集合与数组 陈华的个人主页

    文章信息

    • 标题: JavaScript 5. 对象、集合与数组
    • 发布时间: 2026 年 7 月 19 日
    • 来源: https://physchen.com/zh-Hans/programming/notes/javascript-objects-collections-and-arrays/
    • 摘要: 系统介绍 JavaScript 对象的属性模型、属性描述符、复制与序列化、Map 与 Set、弱引用集合、Date 与高精度计时,以及数组的索引、空位、常用方法和引擎优化边界。

    目录

      JavaScript 5. 对象、集合与数组

      发布于 2026 年 7 月 19 日
      • JavaScript 基础
      • JavaScript
      • Object
      • Array

      对象和数组是 JavaScript 中组织复合数据的主要工具⁠。对象适合表示由命名属性构成的实体⁠,数组适合表示按照索引排列的数据序列⁠,Map 和 Set 则提供了更明确的键值映射与唯一值集合语义⁠。

      理解这些结构时⁠,需要区分三个层次⁠:

      1. 语言语义⁠:ECMAScript 规定属性键⁠、属性描述符⁠、原型查找⁠、数组索引和内置方法的可观察行为⁠;
      2. 逻辑数据模型⁠:程序使用对象⁠、数组和集合表达怎样的数据关系⁠;
      3. 引擎实现⁠:V8⁠、SpiderMonkey 和 JavaScriptCore 可以使用不同的内存布局与优化策略⁠。

      ECMAScript 规范不会规定“⁠原始值一定存放在栈中⁠,对象一定存放在堆中⁠”⁠,也不会要求所有普通对象必须使用哈希表⁠。引擎可以进行逃逸分析⁠、标量替换⁠、内联存储和其他优化⁠,只要不改变程序的可观察结果⁠。

      因此⁠,本章以语言保证为基础⁠,在明确标注时补充具体引擎的实现思路⁠,但不把某个引擎当前采用的存储方式当作 JavaScript 语言本身的永久规则⁠。

      5.1 对象与引用语义

      5.1.1 对象是属性的集合

      ECMAScript 对象由属性和内部槽等状态组成⁠。普通属性由属性键和属性描述符共同定义⁠。

      属性键只能是⁠:

      • 字符串⁠;
      • Symbol⁠。

      属性值可以是任意 JavaScript 值⁠,包括原始值⁠、对象和函数⁠。

      const student = {
        name: "Alice",
        score: 95,
      
        introduce() {
          return `I am ${this.name}.`;
        },
      };

      这里⁠:

      • "name"⁠、"score" 和 "introduce" 是字符串属性键⁠;
      • "Alice" 和 95 是原始值⁠;
      • introduce 对应的属性值是函数对象⁠。

      5.1.2 变量保存的是值

      JavaScript 的赋值和参数传递都以值为基础⁠。对象值具有引用语义⁠,因此两个变量可以引用同一个对象⁠:

      const user = {
        name: "Alice",
      };
      
      const administrator = user;
      
      administrator.name = "Bob";
      
      console.log(user.name); // Bob
      console.log(user === administrator); // true

      user 和 administrator 的求值结果引用同一个对象⁠,所以通过任一变量修改对象⁠,都可以从另一个变量观察到变化⁠。

      重新给其中一个变量赋值⁠,不会改变另一个变量⁠:

      let first = {
        value: 10,
      };
      
      let second = first;
      
      second = {
        value: 20,
      };
      
      console.log(first.value);  // 10
      console.log(second.value); // 20

      不应把这套规则表述为“⁠变量中保存堆地址指针⁠”⁠。ECMAScript 只要求对象身份⁠、赋值和相等比较表现出相应语义⁠,不规定值必须采用何种物理表示⁠。

      5.1.3 对象身份

      两个内容相同但分别创建的对象并不严格相等⁠:

      const first = {
        value: 10,
      };
      
      const second = {
        value: 10,
      };
      
      console.log(first === second); // false

      只有引用同一对象时⁠,严格相等才返回 true⁠:

      const first = {
        value: 10,
      };
      
      const second = first;
      
      console.log(first === second); // true

      JavaScript 没有内置的通用“⁠深层对象相等⁠”运算符⁠。是否相等取决于业务定义⁠,例如⁠:

      • 是否要求属性集合完全相同⁠;
      • 是否忽略属性顺序⁠;
      • 如何处理数组⁠;
      • 如何处理 Date⁠、Map⁠、Set⁠;
      • 如何处理循环引用⁠;
      • 是否比较原型和属性描述符⁠。

      因此⁠,深层比较应根据明确的数据模型实现或使用经过验证的工具⁠。

      5.2 对象的创建

      5.2.1 对象字面量

      对象字面量是最常见的创建方式⁠:

      const user = {
        name: "Alice",
        age: 30,
      };

      属性之间使用逗号分隔⁠。最后一个属性后保留尾随逗号有利于版本控制和自动格式化⁠:

      const settings = {
        theme: "dark",
        language: "zh-CN",
      };

      5.2.2 属性简写

      变量名和属性名相同时⁠,可以使用简写⁠:

      const name = "Alice";
      const score = 95;
      
      const student = {
        name,
        score,
      };

      等价于⁠:

      const student = {
        name: name,
        score: score,
      };

      5.2.3 方法简写

      const user = {
        name: "Alice",
      
        greet() {
          return `Hello, ${this.name}!`;
        },
      };

      方法简写与把普通函数表达式作为属性值在多数调用场景下相似⁠,但并非所有规范细节都完全相同⁠。例如⁠,方法定义可以获得用于 super 的 [[HomeObject]] 语义⁠。

      对象方法与类继承将在第六章进一步讨论⁠。

      5.2.4 计算属性名

      方括号允许在对象创建时计算属性键⁠:

      const fieldName = "score";
      const internalId = Symbol("internalId");
      
      const student = {
        name: "Alice",
        [fieldName]: 95,
        [internalId]: 1001,
      };
      
      console.log(student.score);       // 95
      console.log(student[internalId]); // 1001

      计算表达式的结果会经过属性键转换⁠。如果结果不是 Symbol⁠,就会转换为字符串⁠。

      5.2.5 Object.create

      Object.create(prototype) 创建一个具有指定原型的新对象⁠:

      const animal = {
        eats: true,
      };
      
      const rabbit = Object.create(animal);
      
      rabbit.jumps = true;
      
      console.log(rabbit.jumps); // true
      console.log(rabbit.eats);  // true

      创建无原型对象⁠:

      const dictionary = Object.create(null);
      
      dictionary.name = "Alice";
      
      console.log(dictionary.name); // Alice
      console.log(
        Object.getPrototypeOf(dictionary),
      ); // null

      无原型对象不会继承 Object.prototype 的方法⁠,适合需要纯字符串键容器的少数场景⁠:

      console.log(
        dictionary.toString,
      ); // undefined

      不过⁠,动态键值映射通常优先使用 Map⁠,因为它具有更明确的接口⁠,并支持任意类型的键⁠。

      5.2.6 构造函数与类

      对象也可以通过构造函数或类创建⁠:

      class User {
        constructor(name) {
          this.name = name;
        }
      }
      
      const user = new User("Alice");

      构造⁠、原型链和类继承是第六章的主题⁠。本章只讨论创建后对象的通用属性行为⁠。

      5.3 属性键与属性访问

      5.3.1 点号访问

      点号语法要求属性名能够直接写成标识符名称⁠:

      const user = {
        name: "Alice",
      };
      
      console.log(user.name);

      点号后的名称不会作为变量求值⁠:

      const key = "name";
      
      console.log(user.key); // undefined

      这里访问的是 "key" 属性⁠,而不是变量 key 对应的 "name"⁠。

      5.3.2 方括号访问

      方括号内部是普通表达式⁠:

      const key = "name";
      
      console.log(user[key]); // Alice

      以下属性必须或更适合使用方括号⁠:

      const data = {
        "study physics": true,
        "user-id": 1001,
      };
      
      console.log(
        data["study physics"],
      );
      
      console.log(
        data["user-id"],
      );

      Symbol 属性也必须通过方括号访问⁠:

      const id = Symbol("id");
      
      const user = {
        [id]: 1001,
      };
      
      console.log(user[id]);

      5.3.3 属性键转换

      普通对象的属性键只能是字符串或 Symbol⁠。其他值作为属性键时⁠,会通过 ToPropertyKey 语义转换⁠:

      const object = {};
      
      object[1] = "number key";
      object["1"] = "string key";
      
      console.log(object[1]); // string key

      数字 1 最终成为字符串键 "1"⁠,所以两次赋值操作的是同一属性⁠。

      对象作为普通对象的键时⁠,通常先转换为原始值⁠,再转换为字符串⁠:

      const dictionary = {};
      
      const firstKey = {
        id: 1,
      };
      
      const secondKey = {
        id: 2,
      };
      
      dictionary[firstKey] = "first";
      dictionary[secondKey] = "second";
      
      console.log(
        Object.keys(dictionary),
      ); // ["[object Object]"]
      
      console.log(
        dictionary[firstKey],
      ); // second

      两个普通对象默认都转换为字符串 "[object Object]"⁠,因而发生键名冲突⁠。

      需要使用对象本身作为键时⁠,应使用 Map⁠:

      const map = new Map();
      
      map.set(firstKey, "first");
      map.set(secondKey, "second");
      
      console.log(map.get(firstKey));  // first
      console.log(map.get(secondKey)); // second

      5.4 Symbol 属性键

      5.4.1 Symbol 的唯一性

      const first = Symbol("id");
      const second = Symbol("id");
      
      console.log(first === second); // false

      描述字符串只用于调试⁠,不参与相等判断⁠。

      5.4.2 Symbol 不是私有属性

      Symbol 属性不会被部分常规枚举方法返回⁠:

      const id = Symbol("id");
      
      const user = {
        name: "Alice",
        [id]: 1001,
      };
      
      console.log(
        Object.keys(user),
      ); // ["name"]
      
      console.log(
        Object.getOwnPropertyNames(user),
      ); // ["name"]
      
      console.log(
        Object.getOwnPropertySymbols(user),
      ); // [Symbol(id)]

      但 Symbol 属性并不是真正私有⁠:

      console.log(
        Reflect.ownKeys(user),
      ); // ["name", Symbol(id)]

      只要获得 Symbol 值⁠,外部代码就可以直接访问相应属性⁠。即使没有该 Symbol⁠,反射 API 仍可列出对象自身的 Symbol 属性⁠。

      因此⁠,Symbol 适合⁠:

      • 避免普通字符串键名冲突⁠;
      • 实现协议钩子⁠;
      • 保存不希望出现在常规枚举中的元数据⁠。

      它不适合作为安全边界⁠。类私有字段 #field 将在第六章讨论⁠。

      5.4.3 全局 Symbol 注册表

      Symbol.for() 会根据字符串键从全局 Symbol 注册表中取得或创建 Symbol⁠:

      const first = Symbol.for("app.config");
      const second = Symbol.for("app.config");
      
      console.log(first === second); // true

      Symbol.keyFor() 只对注册表中的 Symbol 返回键⁠:

      const registered = Symbol.for("shared");
      const local = Symbol("local");
      
      console.log(
        Symbol.keyFor(registered),
      ); // shared
      
      console.log(
        Symbol.keyFor(local),
      ); // undefined

      注册表中的 Symbol 可以再次通过相同字符串取得⁠,因此不适合作为弱引用集合中的弱键⁠。现代 ECMAScript 的弱集合允许对象和未注册 Symbol 作为弱持有值⁠,稍后会详细说明⁠。

      5.5 属性存在性与删除

      5.5.1 读取不存在的属性

      读取不存在的属性返回 undefined⁠:

      const user = {
        name: "Alice",
      };
      
      console.log(user.age); // undefined

      但属性也可以明确保存 undefined⁠:

      const user = {
        age: undefined,
      };
      
      console.log(user.age); // undefined

      因此⁠,仅根据读取结果无法判断属性是否存在⁠。

      5.5.2 Object.hasOwn

      判断对象是否具有某个自有属性⁠,推荐使用 Object.hasOwn()⁠:

      const user = {
        age: undefined,
      };
      
      console.log(
        Object.hasOwn(user, "age"),
      ); // true
      
      console.log(
        Object.hasOwn(user, "name"),
      ); // false

      它不会沿原型链查找⁠:

      const animal = {
        eats: true,
      };
      
      const rabbit = Object.create(animal);
      rabbit.jumps = true;
      
      console.log(
        Object.hasOwn(rabbit, "jumps"),
      ); // true
      
      console.log(
        Object.hasOwn(rabbit, "eats"),
      ); // false

      与直接调用 object.hasOwnProperty() 相比⁠,Object.hasOwn() 更安全⁠,因为⁠:

      • 对象可能覆盖同名方法⁠;
      • 对象可能没有 Object.prototype⁠;
      • 调用方式更加明确⁠。

      5.5.3 in 运算符

      in 会检查对象自身及其原型链⁠:

      console.log(
        "jumps" in rabbit,
      ); // true
      
      console.log(
        "eats" in rabbit,
      ); // true

      选择原则⁠:

      • 只关心自有属性⁠:使用 Object.hasOwn()⁠;
      • 需要包含继承属性⁠:使用 in⁠。

      类私有字段还支持特殊的品牌检查语法⁠,例如 #field in object⁠,将在第六章讨论⁠。

      5.5.4 delete 运算符

      delete 用于删除对象属性⁠:

      const user = {
        name: "Alice",
        age: 30,
      };
      
      delete user.age;
      
      console.log(
        Object.hasOwn(user, "age"),
      ); // false

      删除不存在的属性通常返回 true⁠:

      console.log(
        delete user.missing,
      ); // true

      不可配置属性不能被删除⁠:

      const object = {};
      
      Object.defineProperty(
        object,
        "fixed",
        {
          value: 10,
          configurable: false,
        },
      );
      
      console.log(
        delete object.fixed,
      ); // 非严格模式通常为 false

      严格模式下尝试删除不可配置属性会抛出 TypeError⁠。

      delete 删除的是属性⁠,不是词法变量⁠:

      const value = 10;
      
      // SyntaxError in strict mode;
      // 也不应使用 delete 删除变量。
      // delete value;

      数组元素使用 delete 后会形成空位⁠,而不会自动收缩数组⁠:

      const values = [
        "A",
        "B",
        "C",
      ];
      
      delete values[1];
      
      console.log(values.length); // 3
      console.log(1 in values);   // false

      需要删除数组元素并调整后续索引时⁠,应使用 splice() 或非修改型的 toSpliced()⁠。

      5.6 可选链与空值合并

      5.6.1 属性可选链

      如果左侧为 null 或 undefined⁠,可选链返回 undefined⁠:

      const user = null;
      
      console.log(
        user?.profile?.name,
      ); // undefined

      如果左侧存在⁠,则继续执行普通属性访问⁠。

      可选链只针对空值短路⁠,不会吞掉后续访问中产生的其他错误⁠:

      const user = {
        profile: {
          get name() {
            throw new Error("Getter failed");
          },
        },
      };
      
      // 仍然抛出 Error。
      // user?.profile?.name;

      5.6.2 计算属性的短路

      let index = 0;
      const values = null;
      
      const result = values?.[index++];
      
      console.log(result); // undefined
      console.log(index);  // 0

      由于左侧为空值⁠,索引表达式没有求值⁠。

      短路只沿同一条连续可选链传播⁠。使用括号中断链条后⁠,后续访问仍可能报错⁠:

      const user = null;
      
      // TypeError
      // (user?.profile).name;

      5.6.3 可选调用

      const plugin = {
        onReady: undefined,
      };
      
      plugin.onReady?.();

      只有当 onReady 为 null 或 undefined 时⁠,调用被跳过⁠。如果属性存在但不是可调用值⁠,仍会抛出 TypeError⁠:

      const plugin = {
        onReady: "not a function",
      };
      
      // TypeError
      // plugin.onReady?.();

      5.6.4 与空值合并配合

      const city = (
        user?.address?.city
        ?? "Unknown"
      );

      ?? 只把 null 和 undefined 视为缺失⁠,不会覆盖有效的 0⁠、空字符串或 false⁠。

      可选链应只用于业务上允许缺失的节点⁠。对必需数据过度使用 ?.⁠,可能让结构错误延后暴露⁠,并使错误更难定位⁠。

      5.7 属性描述符

      JavaScript 属性不仅具有值⁠,还具有控制读写⁠、枚举和配置行为的属性特征⁠。

      5.7.1 数据属性描述符

      数据属性可以具有以下字段⁠:

      • value⁠;
      • writable⁠;
      • enumerable⁠;
      • configurable⁠。

      普通赋值创建的属性通常可写⁠、可枚举⁠、可配置⁠:

      const user = {};
      
      user.name = "Alice";
      
      console.log(
        Object.getOwnPropertyDescriptor(
          user,
          "name",
        ),
      );

      典型结果为⁠:

      {
        value: "Alice",
        writable: true,
        enumerable: true,
        configurable: true,
      }

      5.7.2 Object.defineProperty

      const user = {};
      
      Object.defineProperty(
        user,
        "id",
        {
          value: 1001,
          writable: false,
          enumerable: false,
          configurable: false,
        },
      );

      读取属性⁠:

      console.log(user.id); // 1001

      由于 enumerable 为 false⁠:

      console.log(
        Object.keys(user),
      ); // []

      由于 writable 为 false⁠,严格模式下赋值会抛出 TypeError⁠:

      "use strict";
      
      // TypeError
      // user.id = 2002;

      由于 configurable 为 false⁠:

      • 不能删除属性⁠;
      • 通常不能重新配置描述符⁠;
      • 不能把数据属性改为访问器属性⁠;
      • 只有少数受限变更仍然允许⁠,例如可写数据属性可以从 writable: true 改为 false⁠。

      需要注意⁠,Object.defineProperty() 中省略的布尔字段默认是 false⁠:

      const object = {};
      
      Object.defineProperty(
        object,
        "value",
        {
          value: 10,
        },
      );
      
      console.log(
        Object.getOwnPropertyDescriptor(
          object,
          "value",
        ),
      );

      该属性默认不可写⁠、不可枚举⁠、不可配置⁠。这与普通赋值创建属性的默认特征不同⁠。

      5.7.3 访问器属性

      访问器属性使用 get 和 set⁠,不直接包含 value 和 writable⁠:

      const user = {
        firstName: "Alice",
        lastName: "Smith",
      
        get fullName() {
          return (
            `${this.firstName} `
            + this.lastName
          );
        },
      
        set fullName(value) {
          const [
            firstName,
            ...remainingParts
          ] = value.trim().split(/\s+/);
      
          this.firstName = firstName;
          this.lastName = remainingParts.join(" ");
        },
      };
      
      console.log(user.fullName);
      // Alice Smith
      
      user.fullName = "John Doe";
      
      console.log(user.firstName); // John
      console.log(user.lastName);  // Doe

      读取 fullName 时调用 getter⁠,赋值时调用 setter⁠。

      访问器属性适合⁠:

      • 派生值⁠;
      • 输入验证⁠;
      • 兼容旧接口⁠;
      • 延迟计算⁠;
      • 封装底层存储字段⁠。

      getter 应避免隐藏昂贵或具有明显副作用的操作⁠,否则普通属性访问会产生意外成本⁠。

      5.7.4 描述符检查

      获取单个自有属性描述符⁠:

      Object.getOwnPropertyDescriptor(
        user,
        "fullName",
      );

      获取所有自有属性描述符⁠:

      Object.getOwnPropertyDescriptors(
        user,
      );

      后者适合需要保留 getter⁠、setter 和属性特征的复制操作⁠:

      const clone = Object.defineProperties(
        {},
        Object.getOwnPropertyDescriptors(
          user,
        ),
      );

      这仍然是浅层复制⁠,并且不会自动复制原对象的原型⁠,除非显式指定⁠:

      const clone = Object.create(
        Object.getPrototypeOf(user),
        Object.getOwnPropertyDescriptors(user),
      );

      5.8 对象完整性控制

      5.8.1 Object.preventExtensions

      const object = {
        value: 10,
      };
      
      Object.preventExtensions(object);

      之后不能添加新的自有属性⁠,但已有属性是否可修改或删除⁠,仍由其描述符决定⁠:

      object.value = 20;
      delete object.value;

      检查⁠:

      console.log(
        Object.isExtensible(object),
      ); // false

      5.8.2 Object.seal

      Object.seal() 会⁠:

      • 阻止添加新属性⁠;
      • 把现有自有属性设为不可配置⁠。

      可写数据属性仍然可以修改⁠:

      const object = {
        value: 10,
      };
      
      Object.seal(object);
      
      object.value = 20;
      
      console.log(object.value); // 20

      检查⁠:

      console.log(
        Object.isSealed(object),
      ); // true

      5.8.3 Object.freeze

      Object.freeze() 会在密封基础上⁠,把现有数据属性设为不可写⁠:

      const settings = {
        theme: "dark",
      };
      
      Object.freeze(settings);

      严格模式下修改会抛出 TypeError⁠:

      "use strict";
      
      // TypeError
      // settings.theme = "light";

      检查⁠:

      console.log(
        Object.isFrozen(settings),
      ); // true

      5.8.4 freeze 是浅层操作

      const settings = Object.freeze({
        appearance: {
          theme: "dark",
        },
      });
      
      settings.appearance.theme = "light";
      
      console.log(
        settings.appearance.theme,
      ); // light

      外层属性 appearance 不可重新赋值⁠,但其引用的嵌套对象没有自动冻结⁠。

      需要深层冻结时⁠,必须遍历对象图并处理⁠:

      • 循环引用⁠;
      • Symbol 属性⁠;
      • 访问器⁠;
      • 不同内置对象⁠;
      • 代理对象⁠;
      • 特定宿主对象⁠。

      深层冻结并不是所有数据模型都需要的默认操作⁠。

      5.9 对象到原始值的转换

      对象参与某些运算时⁠,需要先转换为原始值⁠。

      5.9.1 ToPrimitive

      转换通常遵循以下路径⁠:

      1. 查找并调用 [Symbol.toPrimitive](hint)⁠;
      2. 如果没有该方法⁠,则按照提示调用 valueOf() 和 toString()⁠;
      3. 如果没有任何方法返回原始值⁠,抛出 TypeError⁠。

      提示值通常为⁠:

      • "string"⁠;
      • "number"⁠;
      • "default"⁠。

      5.9.2 Symbol.toPrimitive

      const money = {
        amount: 1000,
      
        [Symbol.toPrimitive](hint) {
          if (hint === "string") {
            return `${this.amount} CNY`;
          }
      
          return this.amount;
        },
      };
      
      console.log(String(money));
      // 1000 CNY
      
      console.log(+money);
      // 1000
      
      console.log(money + 500);
      // 1500

      该方法必须返回原始值⁠:

      const invalid = {
        [Symbol.toPrimitive]() {
          return {};
        },
      };
      
      // TypeError
      // String(invalid);

      5.9.3 普通转换顺序

      没有 [Symbol.toPrimitive] 时⁠:

      • 字符串提示通常先尝试 toString()⁠,再尝试 valueOf()⁠;
      • 数字和默认提示通常先尝试 valueOf()⁠,再尝试 toString()⁠。

      Date 对默认提示具有特殊倾向⁠,其行为更接近字符串提示⁠。

      不应在业务逻辑中大量依赖复杂的隐式对象转换⁠。显式读取属性或调用语义明确的方法通常更易理解⁠:

      money.amount + 500;
      money.toString();

      5.10 自有属性的遍历顺序

      普通对象并非“⁠完全无序⁠”⁠。ECMAScript 为普通对象的自有属性键规定了基本顺序⁠:

      1. 数组索引键按数值升序⁠;
      2. 其他字符串键按属性创建顺序⁠;
      3. Symbol 键按属性创建顺序⁠。
      const id = Symbol("id");
      
      const data = {
        "49": "Germany",
        name: "Apple",
        [id]: "secret",
        "1": "USA",
        title: "Professor",
        "3": "China",
      };
      
      console.log(
        Reflect.ownKeys(data),
      );

      结果顺序为⁠:

      [
        "1",
        "3",
        "49",
        "name",
        "title",
        id,
      ]

      5.10.1 数组索引键的范围

      “⁠看起来像整数的字符串⁠”不一定都是数组索引⁠。数组索引的规范范围和形式比“⁠32 位正整数⁠”更精确⁠。

      例如⁠:

      • "0" 是数组索引⁠;
      • "1" 是数组索引⁠;
      • "4294967294" 是数组索引⁠;
      • "4294967295" 不是数组索引⁠;
      • "01" 不是数组索引⁠;
      • "-1" 不是数组索引⁠;
      • "1.5" 不是数组索引⁠。

      普通对象属性顺序规则中的第一组只针对真正的数组索引键⁠。

      5.10.2 不同枚举 API 的范围

      API自有属性继承属性不可枚举字符串Symbol
      Object.keys()是否否否
      Object.values()是否否否
      Object.entries()是否否否
      Object.getOwnPropertyNames()是否是否
      Object.getOwnPropertySymbols()是否不适用是
      Reflect.ownKeys()是否是是
      for...in是是否否

      for...in 的枚举过程还涉及原型链⁠、重复键抑制和对象在遍历期间发生修改等规则⁠。需要稳定处理数据记录时⁠,通常更适合使用 Object.keys()⁠、Object.entries() 或 Map⁠。

      5.10.3 属性顺序不是数组语义

      尽管对象具有规范化的键顺序⁠,仍不应把普通对象当作列表使用⁠。

      对象适合语义键⁠:

      const user = {
        name: "Alice",
        score: 95,
      };

      数组适合顺序数据⁠:

      const scores = [
        95,
        88,
        91,
      ];

      需要任意类型键并严格按照插入顺序迭代键值对时⁠,使用 Map⁠。

      5.11 对象的浅复制

      5.11.1 展开语法

      const source = {
        name: "Alice",
        profile: {
          score: 95,
        },
      };
      
      const copy = {
        ...source,
      };

      展开语法复制源对象自身的可枚举属性⁠,包括字符串键和 Symbol 键⁠。

      它是浅层复制⁠:

      copy.profile.score = 100;
      
      console.log(
        source.profile.score,
      ); // 100

      两个外层对象不同⁠,但 profile 仍引用同一个嵌套对象⁠:

      console.log(
        source === copy,
      ); // false
      
      console.log(
        source.profile === copy.profile,
      ); // true

      5.11.2 Object.assign

      const target = {};
      
      Object.assign(
        target,
        source,
      );

      Object.assign() 会把源对象自身的可枚举属性依次赋给目标对象⁠,并返回目标对象⁠。

      与对象展开相比⁠,一个重要区别是目标写入语义⁠:

      • Object.assign() 使用普通属性设置⁠,可能触发目标对象的 setter⁠;
      • 对象展开在新对象上创建数据属性⁠,不会以相同方式调用目标 setter⁠。
      const target = {
        set value(nextValue) {
          console.log(
            "setter:",
            nextValue,
          );
        },
      };
      
      Object.assign(
        target,
        {
          value: 10,
        },
      );
      // 输出 setter: 10

      对象展开通常用于创建新对象⁠:

      const copy = {
        ...source,
      };

      Object.assign() 适合明确需要写入现有目标对象的场景⁠。

      5.11.3 浅复制不会保留所有对象结构

      对象展开和 Object.assign() 通常不会完整保留⁠:

      • 原型⁠;
      • 不可枚举属性⁠;
      • 属性描述符⁠;
      • getter 和 setter 本身⁠;
      • 内部槽⁠;
      • 私有字段⁠。

      读取源属性时 getter 可能被执行⁠,得到的结果作为普通值复制⁠:

      const source = {
        get value() {
          console.log("getter called");
          return 10;
        },
      };
      
      const copy = {
        ...source,
      };
      
      console.log(
        Object.getOwnPropertyDescriptor(
          copy,
          "value",
        ),
      );

      copy.value 是普通数据属性⁠,不再是原 getter⁠。

      需要保留原型和描述符时⁠,可以使用⁠:

      const copy = Object.create(
        Object.getPrototypeOf(source),
        Object.getOwnPropertyDescriptors(source),
      );

      这仍然不是通用深层克隆⁠。

      5.12 structuredClone

      5.12.1 结构化克隆

      Web 平台的 structuredClone() 使用结构化克隆算法复制支持的数据⁠:

      const source = {
        createdAt: new Date(),
        tags: new Set([
          "JavaScript",
          "Web",
        ]),
        metadata: new Map([
          ["version", 1],
        ]),
      };
      
      source.self = source;
      
      const copy = structuredClone(source);
      
      console.log(
        copy !== source,
      ); // true
      
      console.log(
        copy.self === copy,
      ); // true
      
      console.log(
        copy.createdAt instanceof Date,
      ); // true
      
      console.log(
        copy.tags instanceof Set,
      ); // true

      它能够处理循环引用⁠,并支持许多内置和 Web 平台类型⁠。

      5.12.2 不能克隆所有值

      以下内容通常不能直接结构化克隆⁠:

      • 函数⁠;
      • DOM 节点⁠;
      • WeakMap⁠;
      • WeakSet⁠;
      • 部分宿主对象⁠;
      • 包含不可序列化成员的对象图⁠。

      失败时会抛出 DataCloneError 类型的 DOMException⁠:

      try {
        structuredClone({
          action() {},
        });
      } catch (error) {
        console.log(error.name);
        // DataCloneError
      }

      5.12.3 原型和属性描述符

      结构化克隆的目标是复制数据⁠,而不是复制任意对象的完整运行时行为⁠。

      不能假定它会保留⁠:

      • 用户自定义类实例的原型链⁠;
      • getter 和 setter⁠;
      • 不可枚举属性⁠;
      • writable⁠、enumerable⁠、configurable 等描述符⁠;
      • 私有字段⁠;
      • 函数行为⁠。

      因此⁠,structuredClone() 适合数据对象⁠,而不是任意行为对象或完整类实例复制⁠。

      5.12.4 转移对象

      某些可转移对象可以转移而不是复制⁠:

      const buffer = new ArrayBuffer(1024);
      
      const copy = structuredClone(
        {
          buffer,
        },
        {
          transfer: [buffer],
        },
      );
      
      console.log(buffer.byteLength);      // 0
      console.log(copy.buffer.byteLength); // 1024

      转移后⁠,原 ArrayBuffer 被分离⁠,不再可正常使用⁠。转移适合 Worker 消息和大型二进制数据⁠,能够避免复制成本⁠,但必须明确原所有者将失去该资源⁠。

      5.12.5 深复制不是默认需求

      深复制会增加时间和内存成本⁠,也会破坏原对象共享关系以外的某些语义⁠。

      在修改嵌套数据时⁠,应先判断实际需求⁠:

      • 只修改第一层⁠:浅复制可能足够⁠;
      • 需要不可变更新⁠:只复制发生变化的路径⁠;
      • 需要跨线程传递⁠:结构化克隆或转移⁠;
      • 需要持久化⁠:选择明确的序列化格式⁠;
      • 需要复制类实例⁠:由类提供显式复制方法⁠。

      5.13 Map

      Map 保存键值对⁠,键和值都可以是任意 JavaScript 值⁠。

      5.13.1 基本操作

      const map = new Map();
      
      const user = {
        id: 1,
      };
      
      map.set("language", "JavaScript");
      map.set(user, "metadata");
      
      console.log(
        map.get("language"),
      ); // JavaScript
      
      console.log(
        map.get(user),
      ); // metadata
      
      console.log(map.size); // 2

      常用接口⁠:

      • set(key, value)⁠;
      • get(key)⁠;
      • has(key)⁠;
      • delete(key)⁠;
      • clear()⁠;
      • size⁠。

      set() 返回 Map 本身⁠,因此可以链式调用⁠:

      const map = new Map()
        .set("first", 1)
        .set("second", 2);

      5.13.2 键相等规则

      Map 使用 SameValueZero 语义区分键⁠。

      它认为⁠:

      NaN 与 NaN 相同
      +0 与 -0 相同
      const map = new Map();
      
      map.set(NaN, "not a number");
      
      console.log(
        map.get(NaN),
      ); // not a number
      
      map.set(+0, "zero");
      
      console.log(
        map.get(-0),
      ); // zero

      对象键按对象身份区分⁠:

      const map = new Map();
      
      map.set(
        {
          id: 1,
        },
        "value",
      );
      
      console.log(
        map.get({
          id: 1,
        }),
      ); // undefined

      两个对象内容相同⁠,但不是同一个键对象⁠。

      5.13.3 插入顺序

      Map 按键值对首次插入顺序进行迭代⁠:

      const map = new Map([
        ["first", 1],
        ["second", 2],
      ]);
      
      for (const [
        key,
        value,
      ] of map) {
        console.log(key, value);
      }

      更新现有键的值不会把它移动到末尾⁠:

      map.set("first", 10);

      删除后重新添加则形成新的插入位置⁠:

      map.delete("first");
      map.set("first", 10);

      5.13.4 遍历

      for (const key of map.keys()) {
        console.log(key);
      }
      
      for (const value of map.values()) {
        console.log(value);
      }
      
      for (const entry of map.entries()) {
        console.log(entry);
      }

      Map 默认迭代器与 entries() 相同⁠:

      console.log(
        map[Symbol.iterator]
        === map.entries,
      ); // true

      forEach() 的参数顺序为 value, key, map⁠:

      map.forEach((
        value,
        key,
      ) => {
        console.log(key, value);
      });

      这与数组 forEach() 的 value, index, array 结构保持对应⁠,但容易误写为 key, value⁠。

      5.13.5 Map 的性能保证

      ECMAScript 不要求 Map 必须使用哈希表⁠。规范要求其实现采用平均访问时间相对于元素数量为次线性的机制⁠,可以是哈希表或其他结构⁠。

      因此⁠,通常可以把 Map 视为高效键值集合⁠,但不能把规范中的内部列表伪代码当作真实实现⁠,也不能保证每次操作都严格为 O(1)O(1)O(1)⁠。

      复杂度的渐进分析已在第三章讨论⁠。

      5.13.6 Map 与普通对象的选择

      选择普通对象⁠:

      • 表示具有明确字段的数据记录⁠;
      • 字段主要是固定字符串或 Symbol⁠;
      • 需要对象字面量和 JSON 生态⁠;
      • 需要继承或属性描述符⁠。

      选择 Map⁠:

      • 键动态变化⁠;
      • 键可能是对象或其他任意值⁠;
      • 频繁增删键值⁠;
      • 需要直接读取 size⁠;
      • 需要明确的插入顺序迭代⁠;
      • 不希望处理原型链属性⁠。

      5.14 Set

      Set 保存不重复的值⁠,也使用 SameValueZero 判断元素是否相同⁠。

      5.14.1 基本操作

      const set = new Set([
        1,
        2,
        2,
        3,
      ]);
      
      console.log(set.size); // 3
      
      set.add(4);
      
      console.log(
        set.has(3),
      ); // true
      
      set.delete(2);

      add() 返回 Set 本身⁠:

      const set = new Set()
        .add("A")
        .add("B");

      5.14.2 去重

      const values = [
        1,
        2,
        2,
        3,
        3,
      ];
      
      const uniqueValues = [
        ...new Set(values),
      ];
      
      console.log(
        uniqueValues,
      ); // [1, 2, 3]

      对象仍然按身份判断⁠:

      const first = {
        id: 1,
      };
      
      const second = {
        id: 1,
      };
      
      const set = new Set([
        first,
        second,
      ]);
      
      console.log(set.size); // 2

      5.14.3 集合运算

      现代 ECMAScript 为 Set 提供集合运算方法⁠:

      const first = new Set([
        1,
        2,
        3,
      ]);
      
      const second = new Set([
        3,
        4,
      ]);
      
      console.log(
        first.union(second),
      ); // Set {1, 2, 3, 4}
      
      console.log(
        first.intersection(second),
      ); // Set {3}
      
      console.log(
        first.difference(second),
      ); // Set {1, 2}
      
      console.log(
        first.symmetricDifference(second),
      ); // Set {1, 2, 4}

      关系判断⁠:

      const subset = new Set([
        1,
        2,
      ]);
      
      const superset = new Set([
        1,
        2,
        3,
      ]);
      
      console.log(
        subset.isSubsetOf(superset),
      ); // true
      
      console.log(
        superset.isSupersetOf(subset),
      ); // true
      
      console.log(
        subset.isDisjointFrom(
          new Set([4, 5]),
        ),
      ); // true

      这些方法返回新集合或布尔值⁠,不会修改调用它们的 Set⁠。

      在需要兼容较旧运行环境时⁠,应检查支持情况或通过工具链提供兼容方案⁠。

      5.15 WeakMap 与 WeakSet

      5.15.1 弱持有语义

      普通 Map 会保持对键的可达关联⁠。只要 Map 自身可达⁠,其对象键通常也会继续被视为可达⁠:

      const map = new Map();
      
      let user = {
        id: 1,
      };
      
      map.set(
        user,
        {
          metadata: "value",
        },
      );
      
      user = null;

      这里 Map 中仍然保存原对象作为键⁠,因此该对象仍可通过 map.keys() 重新取得⁠。

      WeakMap 不以相同方式阻止其键成为垃圾回收候选⁠:

      const metadata = new WeakMap();
      
      let user = {
        id: 1,
      };
      
      metadata.set(
        user,
        {
          lastAccess: Date.now(),
        },
      );
      
      user = null;

      当对象在 WeakMap 之外不再可达时⁠,实现可以在之后回收它及相关条目⁠。

      5.15.2 有效键

      现代 ECMAScript 中⁠,WeakMap 的键以及 WeakSet 的元素可以是⁠:

      • 对象⁠;
      • 未注册的 Symbol⁠。
      const weakMap = new WeakMap();
      
      const objectKey = {};
      const symbolKey = Symbol("local");
      
      weakMap.set(objectKey, "object value");
      weakMap.set(symbolKey, "symbol value");

      注册表 Symbol 不允许作为弱键⁠,因为它能够通过 Symbol.for() 再次取得⁠:

      const registered = Symbol.for("shared");
      
      // TypeError
      // weakMap.set(
      //   registered,
      //   "value",
      // );

      普通原始值不能作为弱键⁠:

      // TypeError
      // weakMap.set(
      //   "key",
      //   "value",
      // );

      运行环境较旧时⁠,未注册 Symbol 作为弱键的支持可能尚未具备⁠,应根据目标环境检查兼容性⁠。

      5.15.3 WeakMap 接口

      WeakMap 提供⁠:

      • set()⁠;
      • get()⁠;
      • has()⁠;
      • delete()⁠。

      它不提供⁠:

      • size⁠;
      • keys()⁠;
      • values()⁠;
      • entries()⁠;
      • forEach()⁠;
      • 默认迭代器⁠。

      如果程序能够枚举弱键⁠,就可以观察垃圾回收发生的时间⁠,从而引入不可预测行为⁠。规范因此不提供枚举机制⁠。

      5.15.4 WeakSet 接口

      WeakSet 提供⁠:

      • add()⁠;
      • has()⁠;
      • delete()⁠。

      它同样不可枚举⁠,也没有 size⁠。

      5.15.5 典型用途

      WeakMap 适合把附加状态关联到对象⁠,而不希望这项关联独自延长对象生命周期⁠:

      const validationState = new WeakMap();
      
      function markValidated(object) {
        validationState.set(
          object,
          true,
        );
      }
      
      function isValidated(object) {
        return (
          validationState.get(object)
          === true
        );
      }

      典型用途包括⁠:

      • 对象元数据⁠;
      • DOM 节点相关状态⁠;
      • 以对象身份为键的缓存⁠;
      • 不改变目标对象结构的外部状态⁠;
      • 访问控制实现细节⁠。

      WeakSet 适合标记对象是否已经处理⁠:

      const visited = new WeakSet();
      
      function processNode(node) {
        if (visited.has(node)) {
          return;
        }
      
        visited.add(node);
      
        // 处理节点。
      }

      5.15.6 不应依赖垃圾回收时机

      user = null;

      只表示移除了一个强引用⁠,不表示对象立即回收⁠。垃圾回收时机由实现决定⁠,程序不能通过 WeakMap 观察某个键何时消失⁠。

      WeakMap 和 WeakSet 可以降低某些不必要的保留风险⁠,但不能自动消除所有内存泄漏⁠。以下内容仍可能长期保留对象⁠:

      • 全局变量⁠;
      • 未移除的事件监听器⁠;
      • 活跃定时器⁠;
      • 普通 Map 或数组⁠;
      • 闭包⁠;
      • DOM 引用⁠;
      • 应用缓存⁠;
      • 其他仍可达对象⁠。

      5.16 垃圾回收与可达性

      5.16.1 语言保证

      ECMAScript 允许实现自动管理对象资源⁠,并通过可达性和弱持有语义约束可观察行为⁠。

      从程序设计角度⁠,可以把以下值视为常见根或活跃入口⁠:

      • 当前正在执行的环境⁠;
      • 全局环境⁠;
      • 宿主保持的回调和对象⁠;
      • 活跃模块⁠;
      • 仍被其他可达对象引用的值⁠。

      只要对象仍然能从活跃入口通过强关联到达⁠,它通常不能被当作不可达资源处理⁠。

      5.16.2 不限定具体垃圾回收算法

      “⁠JavaScript 使用标记—清除算法⁠”并不是完整的语言保证⁠。现代引擎可能组合使用⁠:

      • 分代回收⁠;
      • 标记—清除⁠;
      • 标记—整理⁠;
      • 增量回收⁠;
      • 并发回收⁠;
      • 写屏障⁠;
      • 逃逸分析⁠;
      • 特定对象优化⁠。

      这些属于引擎实现⁠。应用代码只应依赖语言和宿主允许观察的行为⁠。

      5.16.3 循环引用不是自动泄漏

      let first = {};
      let second = {};
      
      first.other = second;
      second.other = first;
      
      first = null;
      second = null;

      两个对象互相引用⁠,但如果没有任何活跃入口能够访问它们⁠,它们仍可以成为垃圾回收候选⁠。

      真正的问题是不再需要但仍然可达的对象⁠,而不是对象图中是否存在环⁠。

      5.16.4 主动清理资源

      垃圾回收只管理内存可达性⁠,不会自动替代所有资源管理⁠。

      仍应显式处理⁠:

      • clearTimeout()⁠;
      • clearInterval()⁠;
      • removeEventListener()⁠;
      • AbortController.abort()⁠;
      • 文件和数据库连接⁠;
      • Observer 解除订阅⁠;
      • WebSocket 关闭⁠;
      • Worker 终止⁠。

      错误处理与资源清理将在第八章继续讨论⁠。

      5.17 JSON 序列化

      JSON 是独立的数据交换格式⁠,不等同于 JavaScript 对象字面量⁠。

      5.17.1 JSON.stringify

      const user = {
        name: "Alice",
        score: 95,
      };
      
      const json = JSON.stringify(user);
      
      console.log(json);
      // {"name":"Alice","score":95}

      格式化输出⁠:

      const json = JSON.stringify(
        user,
        null,
        2,
      );

      第三个参数控制缩进⁠。数值最大按 10 个空格处理⁠,字符串也会被截取到有限长度⁠。

      5.17.2 不同位置中的特殊值

      在对象属性中⁠,以下值通常会被忽略⁠:

      • undefined⁠;
      • 函数⁠;
      • Symbol⁠。
      const object = {
        value: undefined,
        action() {},
        symbol: Symbol("value"),
      };
      
      console.log(
        JSON.stringify(object),
      ); // {}

      在数组中⁠,对应位置通常转换为 null⁠:

      const values = [
        undefined,
        function () {},
        Symbol("value"),
      ];
      
      console.log(
        JSON.stringify(values),
      ); // [null,null,null]

      NaN⁠、Infinity 和 -Infinity 也会序列化为 null⁠:

      console.log(
        JSON.stringify([
          NaN,
          Infinity,
          -Infinity,
        ]),
      ); // [null,null,null]

      5.17.3 BigInt

      默认情况下⁠,序列化 BigInt 会抛出 TypeError⁠:

      // TypeError
      // JSON.stringify({
      //   value: 1n,
      // });

      可以通过 replacer 明确转换⁠:

      const json = JSON.stringify(
        {
          value: 1n,
        },
        (
          key,
          value,
        ) => {
          if (
            typeof value
            === "bigint"
          ) {
            return value.toString();
          }
      
          return value;
        },
      );
      
      console.log(json);
      // {"value":"1"}

      转换为字符串后⁠,需要另行设计反序列化规则⁠,JSON 本身不会保存“⁠这是 BigInt⁠”的类型信息⁠。

      5.17.4 循环引用

      const object = {};
      object.self = object;
      
      // TypeError
      // JSON.stringify(object);

      JSON.stringify() 不能直接表示循环引用⁠。可以⁠:

      • 重新设计数据结构⁠;
      • 使用 replacer 忽略循环边⁠;
      • 使用支持引用图的专用格式⁠;
      • 如果目标是内存复制⁠,使用 structuredClone()⁠。

      5.17.5 replacer 函数

      const user = {
        name: "Alice",
        password: "secret",
        score: 95,
      };
      
      const json = JSON.stringify(
        user,
        (
          key,
          value,
        ) => {
          if (key === "password") {
            return undefined;
          }
      
          return value;
        },
        2,
      );
      
      console.log(json);

      replacer 首先会以空字符串键处理包装后的根值⁠,随后处理嵌套成员⁠。不能只根据 key === "" 假定当前值一定是原始根对象⁠,因为对象内部也可以存在空字符串键⁠。

      5.17.6 replacer 数组

      const json = JSON.stringify(
        user,
        [
          "name",
          "score",
        ],
        2,
      );

      只有列表中指定的字符串或数值化属性名会被纳入对象序列化⁠。

      5.17.7 toJSON

      对象具有 toJSON() 方法时⁠,JSON.stringify() 会使用其返回值继续序列化⁠:

      const room = {
        number: 23,
      
        toJSON() {
          return this.number;
        },
      };
      
      const user = {
        name: "Alice",
        room,
      };
      
      console.log(
        JSON.stringify(user),
      );
      // {"name":"Alice","room":23}

      Date 默认具有 toJSON()⁠,通常产生 ISO 格式字符串⁠:

      const value = {
        createdAt: new Date(
          "2026-07-19T08:00:00Z",
        ),
      };
      
      console.log(
        JSON.stringify(value),
      );

      5.17.8 JSON.parse 与 reviver

      const text = (
        '{"name":"Alice",'
        + '"createdAt":'
        + '"2026-07-19T08:00:00.000Z"}'
      );
      
      const user = JSON.parse(
        text,
        (
          key,
          value,
        ) => {
          if (key === "createdAt") {
            return new Date(value);
          }
      
          return value;
        },
      );
      
      console.log(
        user.createdAt instanceof Date,
      ); // true

      reviver 以自底向上的顺序处理属性⁠。返回 undefined 会删除对应属性⁠。

      接收外部 JSON 时仍需要数据验证⁠。JSON 语法正确不代表数据结构符合业务要求⁠。

      5.17.9 JSON 不是通用深复制工具

      const copy = JSON.parse(
        JSON.stringify(source),
      );

      这种写法只适合明确受限的 JSON 数据⁠。它会改变或丢失⁠:

      • undefined⁠;
      • 函数⁠;
      • Symbol⁠;
      • BigInt⁠;
      • Date 类型⁠;
      • Map⁠;
      • Set⁠;
      • 特殊数值⁠;
      • 原型⁠;
      • 属性描述符⁠;
      • 循环引用⁠;
      • 对象共享关系⁠。

      复制普通结构化数据时⁠,应根据需求选择 structuredClone() 或显式映射⁠。

      5.18 Date

      5.18.1 时间值

      Date 对象表示一个时间值⁠。其核心数值语义是从 1970 年 1 月 1 日 00:00:00 UTC 起计算的毫秒数⁠。

      const date = new Date(
        "2026-07-19T08:00:00Z",
      );
      
      console.log(
        date.getTime(),
      );

      这个时间值在 ECMAScript 中是 Number⁠,不是规范保证的 64 位整数存储字段⁠。

      有效 Date 时间值受到规范范围限制⁠。无效日期的内部时间值为 NaN⁠:

      const invalid = new Date(
        "invalid",
      );
      
      console.log(
        Number.isNaN(
          invalid.getTime(),
        ),
      ); // true

      5.18.2 当前时间

      const timestamp = Date.now();

      它返回接近当前 Unix epoch 毫秒数的数值⁠。

      创建当前时间的 Date⁠:

      const now = new Date();

      5.18.3 本地时间与 UTC

      同一个 Date 表示同一个时间点⁠,但不同方法可以使用本地时区或 UTC 解释⁠:

      const date = new Date(
        "2026-07-19T08:00:00Z",
      );
      
      console.log(date.getHours());
      console.log(date.getUTCHours());

      输出差异取决于运行环境的本地时区⁠。

      本地方法包括⁠:

      • getFullYear()⁠;
      • getMonth()⁠;
      • getDate()⁠;
      • getHours()⁠。

      UTC 方法包括⁠:

      • getUTCFullYear()⁠;
      • getUTCMonth()⁠;
      • getUTCDate()⁠;
      • getUTCHours()⁠。

      getMonth() 和 getUTCMonth() 返回 0 到 11⁠,需要特别注意⁠:

      const date = new Date(
        2026,
        6,
        19,
      );
      
      console.log(
        date.getMonth(),
      ); // 6,表示七月

      5.18.4 日期字符串解析

      ISO 8601 形式通常最明确⁠:

      const utcDate = new Date(
        "2026-07-19T08:00:00Z",
      );

      Z 表示 UTC⁠。

      带显式偏移⁠:

      const taipeiDate = new Date(
        "2026-07-19T16:00:00+08:00",
      );

      两者表示同一时间点⁠:

      console.log(
        utcDate.getTime()
        === taipeiDate.getTime(),
      ); // true

      避免依赖含糊的自然语言日期格式⁠:

      // 不推荐依赖跨环境解析。
      // new Date("07/19/2026");

      不同实现⁠、地区设置和字符串形式可能产生不一致结果⁠。数据交换应优先使用完整 ISO 字符串⁠、明确时区偏移或数值时间戳⁠。

      5.18.5 ISO 输出

      const date = new Date(
        "2026-07-19T08:00:00Z",
      );
      
      console.log(
        date.toISOString(),
      );
      // 2026-07-19T08:00:00.000Z

      toISOString() 总是输出 UTC 表示⁠。

      面向用户显示日期时⁠,不应手工拼接月份⁠、日期和时区名称⁠,而应使用 Intl.DateTimeFormat⁠。国际化将在第七章讨论⁠。

      5.18.6 Date 的局限

      传统 Date 同时承担时间点⁠、本地日历字段和格式化入口⁠,时区语义容易混淆⁠。

      在需要复杂日历⁠、时区⁠、纯日期或持续时间建模时⁠,应根据运行环境支持情况研究 Temporal 或成熟日期时间库⁠。不能在尚未确认目标环境支持时⁠,把较新的 API 当作无条件可用功能⁠。

      5.19 performance.now

      测量代码耗时时⁠,浏览器中通常使用 performance.now()⁠:

      const start = performance.now();
      
      let total = 0;
      
      for (
        let index = 0;
        index < 1_000_000;
        index += 1
      ) {
        total += index;
      }
      
      const end = performance.now();
      
      console.log(
        `Elapsed: ${
          (end - start).toFixed(3)
        } ms`,
      );

      5.19.1 单调时间

      同一时间原点下⁠,performance.now() 基于单调时钟⁠。按时间顺序取得的值之差不应为负数⁠,也不受普通系统时钟校准和用户修改墙上时钟的直接影响⁠。

      Date.now() 表示墙上时间⁠,更适合⁠:

      • 记录事件发生的实际时间⁠;
      • 生成时间戳⁠;
      • 与外部时间系统对齐⁠。

      performance.now() 更适合⁠:

      • 测量持续时间⁠;
      • 性能分析⁠;
      • 动画和调度参考⁠。

      5.19.2 精度不是固定保证

      performance.now() 返回以毫秒为单位的浮点数⁠,但浏览器可以出于安全和隐私原因降低精度或加入时间量化⁠。

      因此⁠,“⁠精度可达微秒级⁠”不能作为所有浏览器和上下文的固定承诺⁠。可靠特征是⁠:

      • 相对于当前全局环境的时间原点⁠;
      • 适合测量时间差⁠;
      • 使用单调时钟语义⁠;
      • 实际分辨率由环境决定⁠。

      5.19.3 微基准测试的局限

      一次简单测量容易受以下因素影响⁠:

      • JIT 编译和预热⁠;
      • 垃圾回收⁠;
      • CPU 频率变化⁠;
      • 浏览器后台工作⁠;
      • 内联和死代码消除⁠;
      • 缓存状态⁠;
      • 运行环境差异⁠。

      严谨性能测试应⁠:

      1. 多次运行⁠;
      2. 预热代码⁠;
      3. 避免测量被优化掉的无效结果⁠;
      4. 报告数据规模和运行环境⁠;
      5. 使用统计分布而不是只看一次结果⁠;
      6. 优先使用性能分析器定位真实瓶颈⁠。

      5.20 数组的语言模型

      5.20.1 数组是特殊对象

      数组是具有特殊 [[DefineOwnProperty]] 行为的异类对象⁠。它对数组索引属性和 length 属性进行特殊处理⁠。

      const values = [
        "A",
        "B",
        "C",
      ];
      
      console.log(
        typeof values,
      ); // object
      
      console.log(
        Array.isArray(values),
      ); // true

      检测数组应使用 Array.isArray()⁠,而不是 typeof⁠。

      5.20.2 数组索引

      const values = [];
      
      values[0] = "A";
      values[1] = "B";

      源码中的数字索引最终对应特殊形式的字符串属性键⁠。

      数组索引范围为⁠:

      0≤i<232−10\leq i<2^{32}-10≤i<232−1

      最大数组索引为⁠:

      232−22^{32}-2232−2

      2 ** 32 - 1 对应的字符串属性不是数组索引⁠:

      const values = [];
      
      values[2 ** 32 - 1] = "named property";
      
      console.log(values.length); // 0

      5.20.3 length 不等于元素数量

      数组的 length 始终大于所有自有数组索引⁠,其值等于最大自有数组索引加一⁠;空数组时为 0⁠。

      const values = [];
      
      values[5] = "A";
      
      console.log(values.length); // 6

      数组只有一个实际元素⁠,但 length 为 6⁠。

      因此⁠,length 表示索引范围上界⁠,不一定表示实际存在的元素数量⁠。

      5.20.4 修改 length

      缩短 length 会删除超出新范围的数组索引属性⁠:

      const values = [
        "A",
        "B",
        "C",
        "D",
      ];
      
      values.length = 2;
      
      console.log(values);
      // ["A", "B"]

      增大 length 会扩大索引范围⁠,但不会自动创建值为 undefined 的元素⁠:

      values.length = 5;
      
      console.log(values.length); // 5
      console.log(2 in values);   // false

      新位置是空位⁠。

      非法长度会抛出 RangeError⁠:

      const values = [];
      
      // RangeError
      // values.length = -1;
      
      // RangeError
      // values.length = 1.5;

      5.20.5 清空数组

      values.length = 0;

      这会删除数组中的索引属性⁠,并保留同一个数组对象身份⁠:

      const values = [
        1,
        2,
        3,
      ];
      
      const reference = values;
      
      values.length = 0;
      
      console.log(
        reference === values,
      ); // true
      
      console.log(
        reference.length,
      ); // 0

      它不会保证垃圾回收立即发生⁠。被删除的元素值如果仍由其他位置引用⁠,也不会因为数组清空而回收⁠。

      如果需要让调用方区分新旧集合⁠,可以创建新数组⁠:

      let values = [
        1,
        2,
        3,
      ];
      
      values = [];

      这只改变当前变量的绑定⁠,其他仍引用旧数组的代码不受影响⁠。

      5.21 稠密数组⁠、空位与 undefined

      5.21.1 空位

      const values = new Array(3);
      
      console.log(values.length); // 3
      console.log(0 in values);   // false

      这三个位置是空位⁠,不是三个明确保存的 undefined⁠。

      显式 undefined⁠:

      const values = [
        undefined,
        undefined,
        undefined,
      ];
      
      console.log(0 in values); // true

      两者在部分读取场景中都得到 undefined⁠,但枚举和数组方法行为可能不同⁠。

      5.21.2 创建空位的常见方式

      const first = [
        1,
        ,
        3,
      ];
      
      const second = new Array(3);
      
      const third = [
        1,
        2,
        3,
      ];
      
      delete third[1];

      不建议在一般业务数据中主动制造空位⁠,因为不同数组方法对空位的处理并不完全一致⁠。

      5.21.3 方法对空位的差异

      map() 跳过空位⁠,并在结果中保留对应空位⁠:

      const values = [
        1,
        ,
        3,
      ];
      
      const mapped = values.map(
        value => value * 2,
      );
      
      console.log(mapped);
      // [2, empty, 6]
      
      console.log(1 in mapped); // false

      forEach() 也跳过空位⁠:

      values.forEach((
        value,
        index,
      ) => {
        console.log(index, value);
      });
      // 只处理索引 0 和 2

      for...of 读取空位时产生 undefined⁠:

      for (const value of values) {
        console.log(value);
      }
      // 1
      // undefined
      // 3

      展开语法也会把空位读取为 undefined⁠:

      console.log([
        ...values,
      ]);
      // [1, undefined, 3]

      includes(undefined) 可以在含空位数组中返回 true⁠:

      console.log(
        values.includes(undefined),
      ); // true

      实际使用时应查阅具体方法语义⁠,不要假设所有迭代方法以同一方式处理空位⁠。

      5.21.4 创建填充值数组

      const zeros = new Array(3).fill(0);
      
      console.log(zeros);
      // [0, 0, 0]

      填充对象时⁠,每个位置会引用同一个对象⁠:

      const rows = new Array(3).fill({});
      
      rows[0].value = 10;
      
      console.log(rows[1].value); // 10

      需要独立对象时⁠:

      const rows = Array.from(
        {
          length: 3,
        },
        () => ({}),
      );

      5.22 创建和转换数组

      5.22.1 数组字面量

      const values = [
        1,
        2,
        3,
      ];

      通常优先使用数组字面量⁠。

      5.22.2 Array 构造器的歧义

      const first = Array(3);
      
      console.log(first.length); // 3

      单个非负整数参数表示长度⁠,而不是唯一元素⁠:

      const second = Array(3, 4);
      
      console.log(second);
      // [3, 4]

      因此⁠,普通数组创建更适合使用字面量⁠、Array.of() 或 Array.from()⁠。

      5.22.3 Array.of

      console.log(
        Array.of(3),
      ); // [3]
      
      console.log(
        Array.of(3, 4),
      ); // [3, 4]

      5.22.4 Array.from

      从可迭代对象创建数组⁠:

      const characters = Array.from(
        "JavaScript",
      );

      从类数组对象创建数组⁠:

      const values = Array.from({
        0: "A",
        1: "B",
        length: 2,
      });
      
      console.log(values);
      // ["A", "B"]

      同时映射⁠:

      const squares = Array.from(
        {
          length: 5,
        },
        (
          value,
          index,
        ) => index ** 2,
      );
      
      console.log(squares);
      // [0, 1, 4, 9, 16]

      5.22.5 展开语法

      const set = new Set([
        1,
        2,
        3,
      ]);
      
      const values = [
        ...set,
      ];

      展开要求值可迭代⁠,而 Array.from() 还可以处理类数组对象⁠。

      5.23 数组中的普通属性

      数组也可以具有非索引属性⁠:

      const values = [
        1,
        2,
        3,
      ];
      
      values.description = "numbers";
      
      console.log(
        values.description,
      ); // numbers
      
      console.log(
        values.length,
      ); // 3

      非索引属性不影响 length⁠。

      不应使用数组模拟关联字典⁠:

      const users = [];
      
      users.alice = {
        score: 95,
      };

      许多数组方法只处理索引范围⁠,不会处理 alice⁠:

      console.log(
        users.map(
          user => user.score,
        ),
      ); // []

      语义键应使用普通对象或 Map⁠。

      5.24 数组方法的修改语义

      数组方法可以按照是否修改原数组分组⁠,但“⁠不修改原数组⁠”不等于“⁠纯函数⁠”⁠。

      回调可以产生副作用⁠,返回的新数组也可能与原数组共享嵌套对象⁠。

      5.24.1 常见原地修改方法

      • push()⁠;
      • pop()⁠;
      • shift()⁠;
      • unshift()⁠;
      • splice()⁠;
      • sort()⁠;
      • reverse()⁠;
      • fill()⁠;
      • copyWithin()⁠。
      const values = [
        1,
        2,
        3,
      ];
      
      values.push(4);
      
      console.log(values);
      // [1, 2, 3, 4]

      5.24.2 常见非修改方法

      • slice()⁠;
      • concat()⁠;
      • map()⁠;
      • filter()⁠;
      • flat()⁠;
      • flatMap()⁠;
      • toSorted()⁠;
      • toReversed()⁠;
      • toSpliced()⁠;
      • with()⁠。
      const values = [
        3,
        1,
        2,
      ];
      
      const sorted = values.toSorted(
        (a, b) => a - b,
      );
      
      console.log(values);
      // [3, 1, 2]
      
      console.log(sorted);
      // [1, 2, 3]

      5.24.3 现代复制型方法

      反转副本⁠:

      const reversed = values.toReversed();

      排序副本⁠:

      const sorted = values.toSorted(
        (a, b) => a - b,
      );

      非破坏增删⁠:

      const updated = values.toSpliced(
        1,
        1,
        100,
      );

      替换指定索引⁠:

      const replaced = values.with(
        1,
        100,
      );

      with() 支持负索引⁠:

      const replacedLast = values.with(
        -1,
        100,
      );

      这些方法便于不可变数据更新⁠,但仍然是浅层复制⁠。

      5.25 尾部与头部操作

      5.25.1 尾部操作

      const values = [];
      
      values.push("A");
      values.push("B");
      
      console.log(
        values.pop(),
      ); // B

      现代引擎通常能高效处理数组尾部追加和删除⁠。具体扩容策略属于引擎实现⁠,通常从算法角度视为均摊常数成本⁠。

      5.25.2 头部操作

      const values = [
        "B",
        "C",
      ];
      
      values.unshift("A");
      
      console.log(
        values.shift(),
      ); // A

      数组头部插入或删除通常需要重新处理后续索引⁠,因此随着数组增大⁠,成本通常线性增长⁠。

      需要大量先进先出操作时⁠,应采用第三章介绍的队列结构⁠,而不是频繁使用 shift()⁠。

      5.26 slice⁠、splice 与 toSpliced

      5.26.1 slice

      slice(start, end) 返回左闭右开区间的浅副本⁠:

      const values = [
        "A",
        "B",
        "C",
        "D",
      ];
      
      const result = values.slice(
        1,
        3,
      );
      
      console.log(result);
      // ["B", "C"]
      
      console.log(values);
      // ["A", "B", "C", "D"]

      负索引从末尾计算⁠:

      console.log(
        values.slice(-2),
      ); // ["C", "D"]

      5.26.2 splice

      splice() 原地修改数组⁠,并返回被删除元素⁠:

      const values = [
        "A",
        "B",
        "C",
        "D",
      ];
      
      const deleted = values.splice(
        1,
        2,
        "X",
        "Y",
      );
      
      console.log(deleted);
      // ["B", "C"]
      
      console.log(values);
      // ["A", "X", "Y", "D"]

      5.26.3 toSpliced

      const values = [
        "A",
        "B",
        "C",
        "D",
      ];
      
      const updated = values.toSpliced(
        1,
        2,
        "X",
        "Y",
      );
      
      console.log(values);
      // ["A", "B", "C", "D"]
      
      console.log(updated);
      // ["A", "X", "Y", "D"]

      5.27 map⁠、filter 与 reduce

      5.27.1 map

      const values = [
        1,
        2,
        3,
      ];
      
      const doubled = values.map(
        value => value * 2,
      );
      
      console.log(doubled);
      // [2, 4, 6]

      map() 适合让每个输入元素对应一个输出元素⁠。

      不要使用 map() 只为了副作用⁠:

      // 不推荐
      values.map(value => {
        console.log(value);
      });

      此时应使用 forEach() 或普通循环⁠。

      5.27.2 filter

      const values = [
        1,
        2,
        3,
        4,
      ];
      
      const evenValues = values.filter(
        value => value % 2 === 0,
      );
      
      console.log(evenValues);
      // [2, 4]

      回调返回真值时保留元素⁠。

      5.27.3 reduce

      const values = [
        1,
        2,
        3,
        4,
      ];
      
      const total = values.reduce(
        (
          accumulator,
          value,
        ) => {
          return accumulator + value;
        },
        0,
      );
      
      console.log(total); // 10

      推荐显式提供初始值⁠。空数组在没有初始值时调用 reduce() 会抛出 TypeError⁠:

      // TypeError
      // [].reduce(
      //   (a, b) => a + b,
      // );

      reduce() 能表达许多聚合⁠,但复杂状态转换可能比普通循环更难读⁠。选择标准应是清晰⁠,而不是把所有处理强行写成 reduce()⁠。

      5.27.4 浅层结果

      const source = [
        {
          value: 1,
        },
      ];
      
      const copy = source.map(
        item => item,
      );
      
      copy[0].value = 2;
      
      console.log(
        source[0].value,
      ); // 2

      map() 创建了新数组⁠,但元素仍可以引用原对象⁠。

      5.28 flat 与 flatMap

      5.28.1 flat

      const values = [
        1,
        [
          2,
          [
            3,
          ],
        ],
      ];
      
      console.log(
        values.flat(),
      ); // [1, 2, [3]]
      
      console.log(
        values.flat(2),
      ); // [1, 2, 3]

      完全展开⁠:

      values.flat(Infinity);

      对于非常深或循环式数据结构⁠,不应盲目使用无限深度⁠。数组本身不能直接包含会被 flat() 无限递归展开的自引用而不引发问题⁠,应避免这种结构⁠。

      5.28.2 flatMap

      flatMap() 相当于先映射⁠,再展开一层⁠,但其语义和实现不必理解为真的创建中间数组后再调用 flat()⁠:

      const sentences = [
        "JavaScript is flexible",
        "Arrays are useful",
      ];
      
      const words = sentences.flatMap(
        sentence => sentence.split(" "),
      );
      
      console.log(words);

      它适合一个输入产生零个⁠、一个或多个输出⁠:

      const values = [
        1,
        2,
        3,
        4,
      ];
      
      const evenSquares = values.flatMap(
        (value) => {
          if (value % 2 !== 0) {
            return [];
          }
      
          return [
            value ** 2,
          ];
        },
      );
      
      console.log(evenSquares);
      // [4, 16]

      5.29 查找与成员判断

      5.29.1 indexOf 与 lastIndexOf

      const values = [
        "A",
        "B",
        "C",
        "B",
      ];
      
      console.log(
        values.indexOf("B"),
      ); // 1
      
      console.log(
        values.lastIndexOf("B"),
      ); // 3

      它们使用严格相等语义⁠,因此不能匹配 NaN⁠:

      console.log(
        [NaN].indexOf(NaN),
      ); // -1

      5.29.2 includes

      console.log(
        [NaN].includes(NaN),
      ); // true

      includes() 使用 SameValueZero⁠,因此能够识别 NaN⁠,并把 +0 与 -0 视为相同⁠。

      只需要判断是否存在时⁠,includes() 通常比 indexOf() !== -1 更直接⁠。

      5.29.3 find 与 findIndex

      const users = [
        {
          id: 1,
          name: "Alice",
        },
        {
          id: 2,
          name: "Bob",
        },
      ];
      
      const user = users.find(
        item => item.id === 2,
      );
      
      console.log(user);

      索引⁠:

      const index = users.findIndex(
        item => item.id === 2,
      );

      未找到时⁠:

      • find() 返回 undefined⁠;
      • findIndex() 返回 -1⁠。

      如果数组元素本身可能是 undefined⁠,仅凭 find() 的返回结果无法区分“⁠找到了值为 undefined 的元素⁠”与“⁠未找到⁠”⁠。此时可以使用索引方法或额外条件⁠。

      5.29.4 findLast 与 findLastIndex

      const values = [
        1,
        2,
        3,
        4,
        5,
      ];
      
      console.log(
        values.findLast(
          value => value % 2 === 0,
        ),
      ); // 4
      
      console.log(
        values.findLastIndex(
          value => value % 2 === 0,
        ),
      ); // 3

      5.29.5 some 与 every

      const values = [
        2,
        4,
        6,
      ];
      
      console.log(
        values.some(
          value => value > 5,
        ),
      ); // true
      
      console.log(
        values.every(
          value => value % 2 === 0,
        ),
      ); // true

      空数组边界⁠:

      console.log(
        [].some(() => true),
      ); // false
      
      console.log(
        [].every(() => false),
      ); // true

      every() 对空数组返回 true⁠,符合逻辑中的空真原则⁠。

      5.30 数组排序

      5.30.1 sort 会修改原数组

      const values = [
        3,
        1,
        2,
      ];
      
      const result = values.sort(
        (a, b) => a - b,
      );
      
      console.log(
        result === values,
      ); // true

      需要保留原数组时使用 toSorted()⁠:

      const sorted = values.toSorted(
        (a, b) => a - b,
      );

      5.30.2 默认比较

      不传比较函数时⁠,数组元素会被转换为字符串⁠,并按 UTF-16 代码单元序列排序⁠:

      const values = [
        1,
        2,
        15,
      ];
      
      values.sort();
      
      console.log(values);
      // [1, 15, 2]

      数值升序⁠:

      values.sort(
        (a, b) => a - b,
      );

      数值降序⁠:

      values.sort(
        (a, b) => b - a,
      );

      5.30.3 比较函数契约

      比较函数应满足一致的排序关系⁠:

      • 返回负数⁠:a 排在 b 前⁠;
      • 返回正数⁠:a 排在 b 后⁠;
      • 返回 0 或 NaN⁠:视为二者在排序中相等⁠。
      const users = [
        {
          name: "Alice",
          score: 90,
        },
        {
          name: "Bob",
          score: 80,
        },
      ];
      
      users.sort(
        (
          first,
          second,
        ) => {
          return (
            first.score
            - second.score
          );
        },
      );

      比较函数应尽量满足⁠:

      • 自反一致性⁠;
      • 反对称性⁠;
      • 传递性⁠;
      • 对相同输入结果稳定⁠;
      • 不依赖不断变化的外部状态⁠。

      不一致的比较函数会产生实现相关或难以预测的排序结果⁠。

      5.30.4 稳定排序

      现代 ECMAScript 要求 Array.prototype.sort() 和 toSorted() 保持稳定性⁠。比较结果为相等的元素⁠,排序后保留原相对顺序⁠。

      const users = [
        {
          name: "Alice",
          group: 1,
        },
        {
          name: "Bob",
          group: 2,
        },
        {
          name: "Carol",
          group: 1,
        },
      ];
      
      const sorted = users.toSorted(
        (
          first,
          second,
        ) => {
          return (
            first.group
            - second.group
          );
        },
      );

      在 group 相同的元素中⁠,Alice 仍位于 Carol 前面⁠。

      规范规定结果稳定⁠,但不规定所有引擎必须使用同一种排序算法⁠,也不提供统一的时间和空间复杂度保证⁠。

      5.30.5 字符串本地化排序

      普通字符串排序基于代码单元顺序⁠,不等同于自然语言字典顺序⁠:

      const names = [
        "张三",
        "Alice",
        "李四",
      ];

      面向用户的本地化排序应使用 Intl.Collator 或 localeCompare()⁠。相关内容在第七章讨论⁠。

      5.31 数组迭代器

      数组提供⁠:

      • keys()⁠;
      • values()⁠;
      • entries()⁠。
      const values = [
        "A",
        "B",
      ];
      
      console.log([
        ...values.keys(),
      ]);
      // [0, 1]
      
      console.log([
        ...values.values(),
      ]);
      // ["A", "B"]
      
      console.log([
        ...values.entries(),
      ]);
      // [[0, "A"], [1, "B"]]

      数组默认迭代器是 values()⁠:

      console.log(
        values[Symbol.iterator]
        === values.values,
      ); // true

      需要索引和值时⁠:

      for (const [
        index,
        value,
      ] of values.entries()) {
        console.log(index, value);
      }

      5.32 TypedArray 概览

      普通数组可以保存不同类型的值⁠,长度可动态变化⁠。需要明确二进制布局和数值类型时⁠,应使用 TypedArray⁠。

      const values = new Float64Array([
        1.5,
        2.5,
        3.5,
      ]);

      TypedArray 包括⁠:

      • Int8Array⁠;
      • Uint8Array⁠;
      • Uint8ClampedArray⁠;
      • Int16Array⁠;
      • Uint16Array⁠;
      • Int32Array⁠;
      • Uint32Array⁠;
      • Float32Array⁠;
      • Float64Array⁠;
      • BigInt64Array⁠;
      • BigUint64Array⁠。

      TypedArray 视图基于 ArrayBuffer 或 SharedArrayBuffer 的二进制数据块⁠。它们⁠:

      • 元素类型固定⁠;
      • 长度通常由底层缓冲区决定⁠;
      • 数值写入时执行相应类型转换⁠;
      • 不支持普通数组的所有动态长度操作⁠;
      • 适合图像⁠、音频⁠、网络协议⁠、文件格式和科学计算数据⁠。

      TypedArray 虽然具有许多数组式方法⁠,但不是普通 Array⁠:

      const values = new Uint8Array([
        1,
        2,
        3,
      ]);
      
      console.log(
        Array.isArray(values),
      ); // false

      二进制数据和共享内存属于更专门的主题⁠,本章只建立基本区别⁠。

      5.33 引擎优化与性能边界

      5.33.1 语言规范不规定内存布局

      ECMAScript 不要求数组永远连续存储⁠,也不要求普通对象使用哈希表⁠。以下内容都属于实现选择⁠:

      • 对象字段是否直接嵌入对象⁠;
      • 是否使用隐藏类或形状⁠;
      • 是否使用内联缓存⁠;
      • 数组元素是否连续⁠;
      • 何时切换为字典表示⁠;
      • 数值是否拆箱⁠;
      • 对象是否被标量替换⁠。

      程序正确性不能依赖这些实现细节⁠。

      5.33.2 V8 的对象形状

      V8 会根据对象属性结构维护内部形状信息⁠,常称为 HiddenClass⁠。以相同顺序创建相同属性的对象⁠,可能共享相同形状⁠,从而帮助优化属性访问⁠。

      function createPoint(
        x,
        y,
      ) {
        return {
          x,
          y,
        };
      }
      
      const first = createPoint(1, 2);
      const second = createPoint(3, 4);

      保持相似对象结构一致⁠,通常有利于优化⁠,也有利于代码可读性⁠。

      不过⁠:

      • HiddenClass 不是 ECMAScript 概念⁠;
      • 其他引擎使用不同术语和策略⁠;
      • 引擎版本会改变优化条件⁠;
      • 单个微小结构差异不一定产生可测性能问题⁠。

      5.33.3 V8 的元素表示

      V8 会区分索引元素和命名属性⁠,并根据元素类型⁠、空位和稀疏程度使用不同内部表示⁠。

      稠密数组通常更容易采用紧凑表示⁠:

      const values = [
        1,
        2,
        3,
        4,
      ];

      高度稀疏数组可能改用字典式表示⁠,以避免为大量空位分配连续存储⁠:

      const values = [];
      
      values[1_000_000] = 1;

      因此⁠,不能把具体引擎在某一版本中的优化策略概括为以下绝对规则⁠:

      • 添加一个普通命名属性必然使数组退化为慢数组⁠;
      • 混合任意两种元素类型必然改为哈希表⁠;
      • 逆序填充必然导致字典模式⁠;
      • 稀疏数组查找必然失去常数级性能⁠。

      具体转换由引擎版本⁠、索引范围⁠、密度⁠、元素类型和运行反馈共同决定⁠。

      5.33.4 不要为推测的优化牺牲结构

      优先级通常应是⁠:

      1. 选择语义正确的数据结构⁠;
      2. 保持代码清晰⁠;
      3. 使用真实数据测量⁠;
      4. 通过性能分析器定位瓶颈⁠;
      5. 针对目标引擎和场景优化⁠;
      6. 重新测量并验证收益⁠。

      不要仅凭“⁠对象一定是哈希表⁠”“⁠数组一定连续⁠”或“⁠某种语法更接近底层⁠”等直觉进行优化⁠。

      5.34 数据结构选型

      需求推荐结构
      表示固定字段的数据记录普通对象
      使用任意类型动态键Map
      保存不重复值Set
      给对象关联不延长生命周期的元数据WeakMap
      标记对象是否已经处理WeakSet
      按整数索引保存有序序列Array
      保存固定数值类型的二进制数据TypedArray
      交换 JSON 数据JSON 兼容对象与数组
      复制支持结构化克隆的数据图structuredClone()
      先进先出的大量操作自定义队列
      需要属性描述符和继承普通对象或类实例

      5.34.1 对象与 Map

      普通对象不是 Map 的低级替代品⁠,Map 也不是所有对象的升级版本⁠。

      对象表达实体⁠:

      const user = {
        id: 1,
        name: "Alice",
      };

      Map 表达映射关系⁠:

      const usersById = new Map([
        [
          1,
          {
            name: "Alice",
          },
        ],
      ]);

      5.34.2 数组与对象

      数组表达顺序⁠:

      const chapters = [
        "Syntax",
        "Functions",
        "Algorithms",
      ];

      对象表达命名字段⁠:

      const chapter = {
        title: "Functions",
        order: 2,
      };

      不要使用数组的普通属性模拟对象字段⁠,也不要用连续数字字符串属性的普通对象替代真正数组⁠。

      5.34.3 可变与不可变更新

      原地修改并非天然错误⁠:

      queue.push(task);

      创建副本也并非天然更优⁠:

      const updated = values.with(
        index,
        nextValue,
      );

      选择取决于⁠:

      • 对象是否由多个调用方共享⁠;
      • 是否需要历史状态⁠;
      • UI 框架是否依赖引用变化⁠;
      • 数据规模⁠;
      • 性能成本⁠;
      • 接口契约⁠;
      • 并发和异步边界⁠。

      关键是让修改策略明确且一致⁠。

      5.35 本章小结

      本章系统介绍了 JavaScript 对象⁠、集合和数组的语言语义与工程用法⁠:

      1. 对象是由属性和内部状态构成的复合值⁠,属性键只能是字符串或 Symbol⁠;
      2. 对象赋值复制的是对象值⁠,多个绑定可以引用同一个对象⁠,不应把规范语义直接解释为固定堆栈指针模型⁠;
      3. 点号访问使用固定属性名⁠,方括号访问会计算表达式并执行属性键转换⁠;
      4. Symbol 可以减少字符串键冲突⁠,但不是私有属性⁠;
      5. Object.hasOwn() 只检查自有属性⁠,in 还会检查原型链⁠;
      6. delete 删除属性⁠,删除数组元素会留下空位而不会自动收缩数组⁠;
      7. 可选链只针对 null 和 undefined 短路⁠,不会隐藏 getter 或函数内部的其他错误⁠;
      8. 数据属性由 value⁠、writable⁠、enumerable 和 configurable 描述⁠,访问器属性使用 getter 和 setter⁠;
      9. preventExtensions()⁠、seal() 和 freeze() 控制不同层级的对象完整性⁠,freeze() 默认只作用于当前对象⁠;
      10. 普通对象自有键按照数组索引⁠、其他字符串和 Symbol 三组规则排列⁠;
      11. 对象展开和 Object.assign() 都是浅复制⁠,但目标写入语义和 setter 行为不同⁠;
      12. structuredClone() 能复制循环数据图和多种内置类型⁠,也支持转移可转移对象⁠,但不能复制任意函数和完整类行为⁠;
      13. Map 支持任意类型键⁠,并按插入顺序迭代⁠;Set 使用 SameValueZero 保存唯一值⁠;
      14. WeakMap 和 WeakSet 不阻止对象或未注册 Symbol 成为垃圾回收候选⁠,因此不能提供枚举接口⁠;
      15. 垃圾回收时机不可预测⁠,弱集合也不能自动解决所有内存保留问题⁠;
      16. JSON 是数据交换格式⁠,不是任意 JavaScript 对象的无损表示⁠,也不是通用深复制工具⁠;
      17. Date 的时间值是从 Unix epoch 起计算的毫秒数⁠,面向交换的数据应使用明确时区或 ISO 表示⁠;
      18. performance.now() 使用单调时间语义⁠,适合测量持续时间⁠,但实际精度可以因安全策略而降低⁠;
      19. 数组是对索引属性和 length 具有特殊处理的异类对象⁠;
      20. 数组 length 是索引范围上界⁠,不一定等于实际存在的元素数量⁠;
      21. 空位与显式 undefined 不同⁠,不同数组方法对空位的处理可能不同⁠;
      22. 修改型数组方法改变原数组⁠,现代复制型方法可以返回 toSorted()⁠、toReversed()⁠、toSpliced() 和 with() 等新数组⁠;
      23. includes() 使用 SameValueZero⁠,能够识别 NaN⁠;indexOf() 使用严格相等语义⁠;
      24. 数组排序默认按字符串的 UTF-16 代码单元顺序进行⁠,数值排序必须提供比较函数⁠;
      25. ECMAScript 要求数组排序稳定⁠,但不规定具体排序算法⁠;
      26. TypedArray 适合具有明确二进制布局和固定数值类型的数据⁠;
      27. V8 的 HiddenClass⁠、元素种类和字典表示属于引擎实现细节⁠,不能作为跨引擎语言规则⁠;
      28. 数据结构选择应首先匹配数据语义⁠,再通过真实测量决定是否需要优化⁠。

      参考资料

      1. javascript.info: Objects
      2. javascript.info: Object references and copying
      3. javascript.info: Garbage collection
      4. javascript.info: Optional chaining
      5. javascript.info: Symbol type
      6. javascript.info: Object to primitive conversion
      7. javascript.info: Property flags and descriptors
      8. javascript.info: Property getters and setters
      9. javascript.info: Object.keys, values, entries
      10. javascript.info: Arrays
      11. javascript.info: Array methods
      12. javascript.info: Map and Set
      13. javascript.info: WeakMap and WeakSet
      14. javascript.info: Date and time
      15. javascript.info: JSON methods, toJSON
      16. MDN JavaScript Guide: Working with objects
      17. MDN JavaScript Guide: Indexed collections
      18. MDN JavaScript Guide: Keyed collections
      19. MDN JavaScript Guide: Representing dates & times
      20. MDN JavaScript Guide: Typed arrays
      21. MDN JavaScript Guide: Enumerability and ownership of properties
      22. MDN JavaScript Guide: Memory management
      23. ECMAScript 2026 Language Specification: ECMAScript Data Types and Values
      24. ECMAScript 2026 Language Specification: Ordinary and Exotic Object Behaviours
      25. ECMAScript 2026 Language Specification: Fundamental Objects
      26. ECMAScript 2026 Language Specification: Keyed Collections
      27. ECMAScript 2026 Language Specification: Indexed Collections
      28. ECMAScript 2026 Language Specification: Structured Data
      29. ECMAScript 2026 Language Specification: Numbers and Dates
      30. HTML Standard: Safe Passing of Structured Data
      31. High Resolution Time
      32. V8: Fast Properties in V8
      上一篇 JavaScript 4. 异步编程与事件循环 2026 年 7 月 19 日 下一篇 JavaScript 6. 原型继承与面向对象编程 2026 年 7 月 19 日
      © 2026 CHEN Hua All rights reserved
      闽ICP备2026003335号 · 粤公网安备44030002014022号
      © Hua Chen / PhysChen.com