第三章:内置对象与数据结构:从 API 到内部原理

第三章:内置对象与数据结构:从 API 到内部原理

摘要: 在掌握了语言的核心语法与函数之后,我们进入了数据处理的领域。本章将深入剖析 JavaScript 中最核心的内置对象与数据结构。我们将超越简单的 API 调用,探讨 Array 在 V8 引擎中的内部表示与性能特征,理解 String 的不可变性原理,并从时间复杂度的角度深度对比 ObjectMapSet 的适用场景。学完本章,您将能够基于底层原理,为不同的业务需求选择最高效的数据结构,并写出更健壮、性能更优的代码。


在本章中,我们将深入探索数据的组织与处理:

  1. 首先,我们将从 Array 开始,不仅学习其丰富的 API,更要理解其作为动态集合的性能考量与函数式编程的应用。
  2. 接着,我们将探讨 String,重点是理解其“不可变性”这一核心原理及其对程序性能的深远影响。
  3. 然后,我们将对 ObjectMapSet 进行深度对比,分析它们作为键值对集合的优劣与底层差异。
  4. 最后,我们将快速浏览 JSONMathDate 等实用工具对象,掌握它们在实际开发中的最佳实践。

3.1. 数组 (Array):动态集合与性能考量

在几乎所有的编程语言中,数组都是一种基础且重要的数据结构。然而,JavaScript 的 Array 与 C++/Java 等语言中的传统数组在底层实现上存在着根本性的区别。

3.1.1. 数组的本质:动态对象而非连续内存

核心原理:传统数组(如 C++ 中的 int arr[10])是一段 连续的、固定大小的内存空间,访问元素(arr[i])可以通过简单的指针运算实现,速度极快 (O(1))。

而 JavaScript 的 Array 本质上是一种特殊的对象,其键是整数索引,但它 并不保证内存的连续性。这赋予了它极大的灵活性(如动态增删、存储不同类型元素),但也带来了性能上的复杂性。

为了优化性能,V8 等现代 JavaScript 引擎内部会将数组分为两种主要模式:

  • 密集数组: 当数组元素是连续的时(例如 [1, 2, 3]),引擎会为其分配连续的内存,实现类似传统数组的快速访问。这是性能最优的模式。
  • 稀疏数组: 当数组存在空位时(例如 const a = [1, , 3]; a[1000] = 5;),引擎会将其降级为更慢的哈希表/字典结构来存储。对稀疏数组的操作会比密集数组慢得多。

3.1.2. 基础操作 API:用法与副作用分析

在对数组进行操作时,一个至关重要的考量是该操作是否会修改原数组。这种修改被称为 副作用可变性

产生副作用(修改原数组)的常用方法:

  • push(): 在数组末尾添加一个或多个元素,返回新的长度。
  • pop(): 删除并返回数组的最后一个元素。
  • shift(): 删除并返回数组的第一个元素。
  • unshift(): 在数组开头添加一个或多个元素,返回新的长度。
  • splice(start, deleteCount, ...items): 万能方法,可以从 start 索引开始,删除 deleteCount 个元素,并插入 items。它返回被删除元素的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const numbers = [10, 20, 30, 40];

// push - 往后方推入数组
console.log("--- push ---");

numbers.push(50)
console.log('更新后的数组:', numbers); // [10, 20, 30, 40, 50]

// pop - 弹出数组最后一个元素
console.log("--- pop ---");
const last = numbers.pop();
console.log('弹出的元素为:', last); // 50
console.log('弹出后的数组为:', numbers); // [10, 20, 30, 40]

// shift - 弹出数组第一个元素
console.log("--- shift ---");
const first = numbers.shift();
console.log('弹出的元素为:', first); // 10
console.log('弹出后的数组为:', numbers); // [20, 30, 40]

// unshift - 往前方推入数组
console.log("--- unshift ---");
numbers.unshift(5);
console.log('更新后的数组:', numbers); // [5, 20, 30, 40]

// splice(start, deleteCount, items) - 可以从 `start` 索引开始,删除 `deleteCount` 个元素,并插入 `items`。
// [5,20,30,40]
console.log("--- splice ---");
const deleted = numbers.splice(1, 2); // 从索引 1 开始删除 2 个元素
console.log('删除的元素为:', deleted); // [20, 30]
console.log('更新后的数组为:', numbers); // [ 5, 40 ]

不产生副作用(返回新数组)的常用方法:

  • slice(start, end): 提取从 startend(不含 end)的元素,返回一个 新数组,原数组不变。
  • concat(...items): 连接多个数组或值,返回一个 新数组
1
2
3
4
5
6
7
8
9
10
11
12
13
const original = [1,2,3,4]
const sliced = original.slice(1,3) // 相当于提取 1 和 2
console.log("---slice---");

console.log('Sliced:', sliced); // [2, 3]
console.log('Original:', original); // [1, 2, 3, 4] (原数组未被修改)

const original_2 = [5,6,7,8]
const concat_arry = original.concat(original_2)
console.log("---concat---");
console.log('Concat:', concat_arry); // [1, 2, 3, 4, 5, 6, 7, 8]
console.log('Original:', original); // [1, 2, 3, 4] (原数组未被修改)

在现代 JavaScript 开发,特别是函数式编程和 React 等框架中,强烈推荐优先使用不产生副作用的方法,以保证数据的可预测性和程序的稳定性。

3.1.3. 高阶函数与函数式编程范式

核心思想: 所谓高阶函数,就是指可以接受函数作为参数,或者将函数作为返回值的函数。map, filter, reduce 是数组最重要的三个高阶函数,它们完美体现了 不变性 原则——即从不修改原数组,而是返回一个全新的、经过处理的数组。

map(callback)

作用: 遍历数组,对每个元素执行 callback 函数,并将每次执行的 返回值 收集起来,组成一个 新的数组 返回。

业务场景: 你有一个商品价格列表,需要生成一个包含税后价格的新列表。

1
2
3
4
5
6
7
8
9
const prices = [100, 200, 350];
const TAX_RATE = 1.1;

// 使用 map 生成税后价格数组
// 在这里我们相当于 map 中进行了一个箭头函数,他会对于数组的每一个元素进行一次函数操作
const pricesWithTax = prices.map(price => price * TAX_RATE);

console.log('Prices with tax:', pricesWithTax);
console.log('Original prices:', prices); // 原数组不受影响

filter(callback)

作用: 遍历数组,对每个元素执行 callback 函数。如果 callback 返回 true,则保留该元素,否则丢弃。最后将所有保留的元素组成一个 新的数组 返回。

业务场景: 你有一个用户列表,需要筛选出所有已成年的用户。

1
2
3
4
5
6
7
8
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 17 },
{ name: 'Cathy', age: 30 }
];

const adults = users.filter(user => user.age > 18);
console.log('Adult users:', adults);

reduce(callback, initialValue)

作用: 数组的“聚合器”。它接收一个回调函数和一个可选的初始值。它会遍历数组,将上一次回调的返回值(accumulator)和当前元素(currentValue)传入下一次回调,最终将数组“减少”为一个单一的值。

业务场景: 计算购物车中所有商品的总价。

1
2
3
4
5
6
7
8
9
10
11
12
const cart = [
{ product: 'Laptop', price: 1200 },
{ product: 'Mouse', price: 50 },
{ product: 'Keyboard', price: 100 }
];

// 0 是 accumulator 的初始值
const totalPrice = cart.reduce((accumulator, currentItem) => {
return accumulator + currentItem.price;
}, 0);

console.log('Total price:', totalPrice); // 1350

3.1.4. 排序与搜索的陷阱

sort([compareFunction])

sort() 方法会 就地 对数组元素进行排序(即修改原数组)。它最大的陷阱在于其默认行为。

核心陷阱: 如果不提供 条件sort() 会将所有元素转换为 字符串,然后按 UTF-16 编码顺序进行排序。

1
2
3
4
const numbers = [1, 10, 2, 21, 5];
numbers.sort(); // 默认排序

console.log(numbers);

看到结果 [ 1, 10, 2, 21, 5 ] 了吗?因为 “10” 在字符串比较中排在 “2” 的前面。对数字数组排序时,必须提供比较函数。

解决方案: 提供一个比较函数 (a, b)

  • 如果返回 a - b,则为升序排序。
  • 如果返回 b - a,则为降序排序。
1
2
3
4
const numbers = [1, 10, 2, 21, 5];
// 提供比较函数以实现正确的数字升序排序
numbers.sort((a, b) => a - b);
console.log(numbers);

find() vs indexOf()

  • indexOf(value): 返回指定 value 在数组中首次出现的 索引,如果不存在则返回 -1。它只能用于查找原始类型值。
  • find(callback): 返回数组中满足 callback 函数的 第一个元素的值,如果不存在则返回 undefined。它非常适合用来查找对象数组中的特定对象。
1
2
3
4
5
6
7
8
9
10
11
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 17 },
{ name: 'Cathy', age: 30 }
];

const indexOfUser = users.findIndex(user => user.name == "Bob")
console.log(indexOfUser); // 1

const findUser = users.find(user => user.name == "Bob")
console.log(findUser); // { name: 'Bob', age: 17 }

3.1.5. 迭代协议:for...of 背后的原理

核心原理: for...of 循环之所以能遍历数组、字符串、Map、Set 等,是因为这些对象都遵守了 Iterable 协议

一个对象要成为“可迭代”的,它必须实现一个 [Symbol.iterator] 方法。这个方法返回一个 迭代器 (Iterator) 对象,该对象有一个 next() 方法。每次调用 next(),它会返回一个形如 { value: ..., done: boolean } 的对象,直到遍历结束 done 变为 true

for...of 循环就是这个过程的语法糖。理解这一点有助于你明白为什么 for...of 不能用于遍历普通对象(因为它们默认不是可迭代的)。

1
2
3
4
5
6
const arr = ['a', 'b'];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

3.2. 字符串 (String):不可变性与高效处理

3.2.1. 字符串的不可变性原理

核心原理: 与数组不同,JavaScript 中的字符串是 不可变的。这意味着一旦一个字符串被创建,它的内容就不能被改变。所有看起来像在修改字符串的方法(如 replace(), toUpperCase()),实际上都是在 返回一个全新的字符串,而原始字符串保持不变。

1
2
3
4
5
6
7
8
9
let str = "hello";
let upperStr = str.toUpperCase();

console.log('upperStr:', upperStr); // "HELLO"
console.log('original str:', str); // "hello" (原字符串未变)

// 试图修改字符串的某个字符是无效的
str[0] = 'H';
console.log('After modification attempt:', str); // "hello"

性能影响: 字符串的不可变性意味着在循环中用 ++= 来拼接大量字符串时,性能会很差。因为每次拼接都会创建一个新的中间字符串,并可能引发垃圾回收。

痛点场景:

1
2
3
4
5
let longString = "";
// 性能不佳!会创建大量中间字符串
for (let i = 0; i < 10000; i++) {
longString += "text";
}

解决方案: 在需要拼接大量字符串时,更高效的做法是先将各部分放入一个数组,最后用 join('') 方法一次性合并。

1
2
3
4
5
const parts = [];
for (let i = 0; i < 10000; i++) {
parts.push("text");
}
const longString = parts.join(''); // 高效得多

3.2.2. 现代字符串操作

模板字符串 (Template Literals)

ES6 引入的模板字符串(使用反引号 `)是处理动态文本的最佳方式,它支持内嵌表达式 ${...} 和多行文本,完全取代了传统的 + 拼接方式。

1
2
3
4
5
6
7
const user = { name: 'Prorise', plan: 'Premium' };
const welcomeMessage = `
Hello ${user.name},
Welcome to our service.
Your current plan is: ${user.plan}.
`;
console.log(welcomeMessage);

常用 API

很多时候,我们不需要记忆所有的 API,API 永远都只是一个方法,想要什么值就往里传,在学习过很多编程语言之后我相信您就能理解,再多的方法都不如一个场景使用到的思路来的实在。

方法/属性说明返回值
length属性,获取字符串的长度。Number
toLowerCase() / toUpperCase()转换为全小写或全大写。常用于不区分大小写的比较。String
trim()移除字符串两端的空白字符(空格、制表符、换行符等)。String
includes(substring)检查是否包含子字符串。Boolean
startsWith(str) / endsWith(str)检查字符串是否以指定子字符串开头或结尾。Boolean
indexOf(substring)查找子字符串首次出现的位置索引,如果不存在则返回 -1。Number
slice(startIndex, endIndex)提取子字符串,返回一个新字符串。String
split(separator)按分隔符将字符串分割成数组。Array
replace(searchValue, newValue)替换 第一个 匹配的子字符串或模式。String
replaceAll(searchValue, newValue)替换 所有 匹配的子字符串或模式。String
padStart(len, str) / padEnd(len, str)用指定字符串在开头或末尾进行填充,直到达到目标长度。常用于格式化数字(如补零)。String
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 字符串常用方法演示
let text = "Hello, JavaScript world! ";
let paddedText = " some text ";

// length - 获取字符串长度 (属性)
console.log(text.length); // 25 (注意末尾的空格)

// toLowerCase() / toUpperCase() - 大小写转换
console.log(text.toLowerCase()); // "hello, javascript world! "
console.log(text.toUpperCase()); // "HELLO, JAVASCRIPT WORLD! "

// trim() - 移除两端空白
console.log(paddedText.trim()); // "some text"

// includes() - 检查是否包含子字符串
console.log(text.includes("JavaScript")); // true
console.log(text.includes("Python")); // false

// startsWith() / endsWith() - 检查开头或结尾
console.log(text.startsWith("Hello")); // true
console.log(text.trim().endsWith("!")); // true (先移除末尾空格再判断)

// indexOf() - 查找索引
console.log(text.indexOf("Java")); // 7
console.log(text.indexOf("Python")); // -1

// slice() - 提取子字符串
console.log(text.slice(0, 5)); // "Hello"
console.log(text.slice(7, 17)); // "JavaScript"

// split() - 按分隔符分割成数组
console.log(text.trim().split(" ")); // ["Hello,", "JavaScript", "world!"]
console.log(text.split(",")); // ["Hello", " JavaScript world! "]

// replace() - 替换第一个匹配项
console.log(text.replace("JavaScript", "Node.js")); // "Hello, Node.js world! "

// replaceAll() - 替换所有匹配项
let repeated = "apple, apple, apple";
console.log(repeated.replaceAll("apple", "orange")); // "orange, orange, orange"

// padStart() - 头部填充
let month = "5";
console.log(month.padStart(2, "0")); // "05" (常用于日期格式化)

3.2.3. 正则表达式 (RegExp) 入门

业务场景: 假设你需要验证用户输入的邮箱地址是否合法,或者从一段文本中提取所有电话号码。用 if/else 等传统方法处理会非常繁琐且极易出错。这正是正则表达式的用武之地。

重要提示: 正则表达式是一种独立于任何编程语言的、强大的 文本模式匹配工具。它有自己的一套语法规则。本节的重点不是深入讲解正则表达式本身的每一个规则,而是 介绍如何在 JavaScript 中创建和使用它。如果您对元字符不熟悉,可以查阅 MDN 或其他专业教程进行学习,当然,现在 Ai 对于正则表达式的编写已经比人类强大得多得多了,我强烈建议如果有需求,可以直接找 AI 写正则表达式,完全没有问题

在 JavaScript 中创建正则表达式

  1. 字面量创建 (推荐): 语法为 /pattern/flags,在脚本加载时编译,性能更高。

    1
    const regex = /[a-z]+/g; // 匹配所有小写字母组合
  2. 构造函数创建: 语法为 new RegExp("pattern", "flags"),在运行时编译,适用于模式是动态的(例如,来自用户输入)场景。

    1
    2
    let userInput = "abc";
    const dynamicRegex = new RegExp(userInput, "i"); // 根据用户输入动态创建,忽略大小写

核心概念速览

分类符号说明
常用元字符\d匹配一个数字 (等同于 [0-9])
\w匹配字母、数字、下划线 (等同于 [A-Za-z0-9_])
\s匹配一个空白字符(空格、制表符、换行符等)
.匹配除换行符外的任意单个字符
量词+匹配前面的表达式 一个或多个
*匹配前面的表达式 零个或多个
?匹配前面的表达式 零个或一个
边界^匹配输入的 开头
$匹配输入的 结尾
字符集[]匹配方括号内的任意一个字符,如 [aeiou]
修饰符 (Flags)g全局 (Global) 匹配,查找所有匹配项而非第一个
i忽略大小写 (Ignore case) 匹配
m多行 (Multiline) 匹配,使 ^$ 匹配行的开头/结尾

在 JavaScript 中的实战方法

  • regexp.test(string): 验证。检查字符串是否匹配模式,返回 truefalse。这是最常用的验证方法。
  • string.match(regexp): 提取。返回一个包含匹配结果的数组。如果正则有 g 标志,返回所有匹配的子串;否则只返回第一个匹配项及其捕获组的详细信息。
  • string.replace(regexp, newValue): 替换。查找匹配项并用新值替换。结合 g 标志可以实现全局替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// --- 1. 使用 .test() 进行验证 ---
// 邮箱正则表达式:简单的邮箱格式检查
// 匹配:任意字符 + @ + 任意字符 + . + 任意字符
const emailRegex = /.+@.+\..+/;
const email1 = "test@prorise.com";
const email2 = "invalid-email@";

console.log(`是否 '${email1}' 有效? ${emailRegex.test(email1)}`); // 是否 'test@prorise.com' 有效? true
console.log(`是否 '${email2}' 有效? ${emailRegex.test(email2)}`); // 是否 'invalid-email@' 有效? false

// --- 2. 使用 .match() 进行提取 ---
const text = "My numbers are 123 and 456, not 789.";
const numberRegex = /\d+/g; // \d+ 匹配一个或多个数字, g 表示全局查找

console.log(text.match(numberRegex)); // ["123", "456", "789"]

// --- 3. 使用 .replace() 进行替换 ---
const message = "JavaScript is great. I love JavaScript!";
const jsRegex = /javascript/ig; // i 表示忽略大小写, g 表示全局
// 将所有 "JavaScript" (不区分大小写) 替换为 "JS"
console.log(message.replace(jsRegex, "JS")); // "JS is great. I love JS!"

3.3. 键值对集合:Object, Map, Set 的深度对比

在 JavaScript 中,存储键值对数据最常用的方式是 Object。但 ES6 引入的 MapSet 提供了更专业、更高效的解决方案。

3.3.1. 作为字典的 Object

优势: 语法简洁,易于创建和访问。
原生缺陷:

  1. 键类型限制: 对象的键只能是 StringSymbol 类型。任何非字符串的键都会被隐式转换为字符串。
  2. 原型链污染风险: 如果不小心使用了 __proto__ 等内置属性名作为键,可能会覆盖原型链上的方法,导致意外行为或安全漏洞。
  3. 迭代不便: 遍历对象的属性需要 for...inObject.keys(),不如 MapSet 的直接迭代方便。

3.3.2. Map:为“字典”场景而生的数据结构

Map 是一个真正的哈希表,它的设计完全是为了高效地存储和检索键值对。

与 Object 的核心对比:

特性MapObject
键的类型任意类型 (包括对象、函数)String 或 Symbol
键的顺序按插入顺序历史上无序,现代引擎大多实现有序,但不保证
大小获取.size 属性 (O(1))Object.keys().length (O(n))
迭代直接可迭代 (for...of, forEach)需要辅助方法 (Object.keys, values, entries)
性能在频繁增删键值对的场景下,通常 更优在仅有少量固定属性时性能极佳
原型无原型链,不会与内置属性冲突有原型链,可能存在键名冲突

业务场景: 当你需要一个键不是字符串的字典,或者需要频繁地对集合进行增删操作时,Map 是不二之选。

1
2
3
4
5
6
7
8
9
10
const user1 = { id: 1 };
const user2 = { id: 2 };

const roles = new Map();
// 使用对象作为键
roles.set(user1, 'Admin');
roles.set(user2, 'Editor');

console.log('Role of user1:', roles.get(user1));
console.log('Map size:', roles.size);

3.3.3. Set:唯一值的数学集合

Set 对象允许你存储任何类型的 唯一值,无论是原始值或者是对象引用。

与 Array 的核心对比:

业务场景: 数组去重、检查一个值是否存在于一个大集合中。

场景SetArray
保证唯一性天生如此需要手动去重(如 filter + indexOf
成员检查set.has(value) (O(1))array.includes(value) (O(n))

性能差异是关键。检查一个元素是否存在于一个有 100 万个元素的 Set 中,速度几乎和检查 10 个元素一样快。而对于数组,则需要遍历整个数组,速度会慢 10 万倍。

1
2
3
4
5
6
7
8
9
10
11
const numbers = [1, 2, 2, 3, 4, 4, 5];

// 1. 数组去重
const uniqueNumbers = new Set(numbers);
console.log('Unique numbers:', uniqueNumbers);
// 可轻松转回数组 通过...解构赋值语法将他拆散变为数组
console.log('Back to array:', [...uniqueNumbers]);

// 2. 成员检查
console.log('Does Set have 3?', uniqueNumbers.has(3)); // true
console.log('Does Set have 6?', uniqueNumbers.has(6)); // false

3.4. 其他实用内置对象

3.4.1. JSON:通用数据交换格式

JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,已成为 Web API 通信的事实标准。

  • JSON.stringify(JavaScript值[, 转换函数, 缩进]): 将 JavaScript 的值(如对象、数组)转换为 JSON 字符串

    • JavaScript值: 必需,要被转换成 JSON 字符串的对象、数组或其他值。
    • 转换函数: 可选,一个函数,用于在序列化过程中改变值的行为。
    • 缩进: 可选,用于美化输出的 JSON 字符串,可以是数字(代表空格数)或字符串(如 '\t')。
  • JSON.parse(JSON字符串[, 还原函数]): 将一个 JSON 字符串 解析为对应的 JavaScript 值。

    • JSON字符串: 必需,一个符合 JSON 格式规范的字符串。
    • 还原函数: 可选,一个函数,用于在解析后对生成的值进行转换。

核心规范与安全:

  • JSON 格式比 JS 对象字面量更严格:键必须是双引号包裹的字符串。
  • JSON 不能表示 undefined, Symbol, 函数等。
  • 在解析来自不受信任来源的 JSON 字符串时要小心,JSON.parse 本身是安全的,但解析后的数据可能被恶意利用。reviver 参数可用于在解析过程中对数据进行处理和校验。
1
2
3
4
5
6
const user = { name: "Prorise", id: 1, joined: new Date() };
const jsonString = JSON.stringify(user, null, 2); // 第三个参数用于格式化输出
console.log(jsonString);

const parsedUser = JSON.parse(jsonString);
console.log(parsedUser.name);

3.4.2. Math 与 Date

  • Math: 一个静态对象,提供了常用的数学常数和函数,如 Math.random() (生成 0-1 之间的随机数), Math.max(), Math.floor() 等。
  • Date: 用于处理日期和时间。创建一个 new Date() 实例来表示特定时间点。

实践建议: Date 对象的 API 在处理时区等复杂场景时显得笨拙且易出错。在生产环境中,强烈推荐使用成熟的第三方库,如 date-fnsDay.js,它们提供了更友好、更可靠的 API,同样的 Math 也是一样,对于这些对象的 Api 我们无需细扣,用到什么我们查什么即可


3.5. 本章核心原理速查与高频面试题

核心原理速查

概念核心原理关键影响
JS 数组本质是特殊对象,非连续内存动态灵活,但需警惕稀疏数组带来的性能下降。
副作用/不变性操作是否修改原数据不可变操作更利于代码的可预测性和调试。
高阶函数接受或返回函数的函数map, filter, reduce 是函数式编程的核心,遵循不变性。
字符串不可变性字符串一旦创建无法更改频繁拼接字符串时,应使用 Array.join() 以避免性能问题。
Map vs ObjectMap 键类型任意,性能更优在非字符串键或频繁增删的字典场景,应使用 Map。
Set vs ArraySet 保证唯一性,has 检查在去重和成员检查场景,Set 性能远超 Array。

3.6 课后小结

面试官深度追问
2025-08-27

假设有一个包含上千个用户对象的数组,我需要频繁地检查某个特定用户(通过其对象引用)是否存在于这个数组中。你会选择什么数据结构,为什么?

我会选择 Set 而不是 Array

具体解释一下。

原因是时间复杂度。如果使用数组,每次检查都需要调用 array.includes(userObject),这是一个 O(n) 操作,意味着在最坏的情况下,它需要遍历整个数组来找到该用户。对于上千个元素,这会越来越慢。

Set 内部使用类似哈希表的结构来存储数据。检查一个成员是否存在,使用 set.has(userObject),其平均时间复杂度是 O(1),几乎是瞬时的,不受集合大小的影响。因此,对于频繁的成员检查场景,Set 的性能优势是压倒性的。

很好。看来你对于他们两的理解的还不错