面试题答案
一键面试1. 内存占用
- 数组(Array):
- 内存占用相对固定,连续存储元素。如果数组元素类型相同,内存使用效率较高。例如
let arr = [1, 2, 3, 4];
,数组按顺序存储这几个数字,占用的内存空间与元素个数和元素类型有关。但如果数组中包含不同类型元素,可能会因类型转换等额外占用更多内存。
- 内存占用相对固定,连续存储元素。如果数组元素类型相同,内存使用效率较高。例如
- Set:
- 内存占用通常比数组略高,因为Set需要额外的空间来存储哈希表结构以保证元素唯一性。例如
let set = new Set([1, 2, 3, 4]);
,除了存储元素本身,还需要空间来维护哈希表以快速查找元素是否已存在。
- 内存占用通常比数组略高,因为Set需要额外的空间来存储哈希表结构以保证元素唯一性。例如
- Map:
- 内存占用较高,因为每个键值对都需要存储键和值,并且也依赖哈希表结构。例如
let map = new Map([['a', 1], ['b', 2]]);
,不仅要存储值1和2,还要存储键'a'
和'b'
,同时哈希表结构也占用空间。
- 内存占用较高,因为每个键值对都需要存储键和值,并且也依赖哈希表结构。例如
- 生成器(Generator):
- 内存占用较低,因为生成器按需生成值,而不是一次性存储所有值。例如
function* gen() { yield 1; yield 2; yield 3; } let g = gen();
,生成器g
在任何时刻只需要存储当前生成的值和生成器的状态,而不是像数组那样一次性存储所有的1、2、3。
- 内存占用较低,因为生成器按需生成值,而不是一次性存储所有值。例如
2. 遍历速度
- 数组(Array):
- 遍历速度较快,尤其是使用
for
循环。因为数组元素在内存中连续存储,CPU缓存命中率高。例如:
let arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }
- 遍历速度较快,尤其是使用
- Set:
- 遍历速度与数组相当,但略慢于数组。因为Set的内部结构,遍历需要通过其内部的迭代器逻辑,而不是像数组那样简单的按顺序访问内存。例如:
let set = new Set([1, 2, 3, 4]); for (let value of set) { console.log(value); }
- Map:
- 遍历速度相对较慢,因为需要同时遍历键和值,并且Map内部的迭代器逻辑也相对复杂。例如:
let map = new Map([['a', 1], ['b', 2]]); for (let [key, value] of map) { console.log(key, value); }
- 生成器(Generator):
- 遍历速度依赖于生成值的逻辑,如果生成值的计算简单,遍历速度可以很快。但如果生成值的计算复杂,遍历速度会受影响。例如上述生成器示例,每次
yield
的值简单,遍历速度相对快;若yield
的值是复杂计算结果,遍历速度会慢。
- 遍历速度依赖于生成值的逻辑,如果生成值的计算简单,遍历速度可以很快。但如果生成值的计算复杂,遍历速度会受影响。例如上述生成器示例,每次
3. 数据插入删除
- 数组(Array):
- 插入:在数组开头或中间插入元素性能较差,因为需要移动后续元素。例如
let arr = [1, 2, 3, 4]; arr.splice(1, 0, 5);
,从索引1处插入5,后续元素2、3、4都要向后移动。在数组末尾插入元素(push
方法)性能较好。 - 删除:删除数组中间或开头元素性能较差,同样需要移动元素。例如
arr.splice(1, 1);
删除索引1处元素,后续元素要向前移动。删除数组末尾元素(pop
方法)性能较好。
- 插入:在数组开头或中间插入元素性能较差,因为需要移动后续元素。例如
- Set:
- 插入:插入操作性能较好,平均时间复杂度为O(1),因为Set使用哈希表结构,能快速判断元素是否已存在并插入。例如
let set = new Set([1, 2, 3]); set.add(4);
。 - 删除:删除操作性能也较好,平均时间复杂度为O(1)。例如
set.delete(3);
。
- 插入:插入操作性能较好,平均时间复杂度为O(1),因为Set使用哈希表结构,能快速判断元素是否已存在并插入。例如
- Map:
- 插入:插入操作性能较好,平均时间复杂度为O(1),类似Set使用哈希表结构来存储键值对。例如
let map = new Map([['a', 1]]); map.set('b', 2);
。 - 删除:删除操作性能也较好,平均时间复杂度为O(1)。例如
map.delete('a');
。
- 插入:插入操作性能较好,平均时间复杂度为O(1),类似Set使用哈希表结构来存储键值对。例如
- 生成器(Generator):
- 生成器本身不直接支持插入删除操作,因为它是按需生成值的序列,不是像数组、Set、Map那样的数据集合存储结构。
4. 应用场景及选择
- 数组(Array):
- 场景:当需要有序存储数据,且对元素顺序有严格要求,同时遍历操作频繁时,优先选择数组。例如存储学生成绩列表,按考试顺序存储成绩,方便后续按顺序统计分析。
- 示例:
let scores = [85, 90, 78]; let total = 0; for (let score of scores) { total += score; } let average = total / scores.length;
- Set:
- 场景:当需要存储唯一值,且对元素顺序无严格要求,同时需要快速判断元素是否存在时,优先选择Set。例如统计一篇文章中不重复的单词。
- 示例:
let text = "the cat jumps over the dog"; let words = text.split(' '); let uniqueWords = new Set(words); console.log(uniqueWords.size);
- Map:
- 场景:当需要存储键值对数据,且对键值对的查找、插入、删除操作频繁时,优先选择Map。例如存储用户ID和用户信息的对应关系。
- 示例:
let users = new Map(); users.set(1, { name: 'Alice', age: 25 }); users.set(2, { name: 'Bob', age: 30 }); let user = users.get(1);
- 生成器(Generator):
- 场景:当需要处理大量数据,但又不想一次性加载到内存中,或者数据生成需要复杂逻辑且按需生成时,优先选择生成器。例如读取大文件的每一行数据。
- 示例:
function* readLargeFile(file) { let line; while ((line = file.readLine())) { yield line; } } let file = { lines: ["line1", "line2", "line3"], readLine: function () { return this.lines.shift(); } }; let fileGen = readLargeFile(file); for (let line of fileGen) { console.log(line); }
5. 性能优化方向
- 引擎层面:
- 数组:优化内存分配策略,提高连续内存分配的效率,减少碎片化。例如,V8引擎可以在数组初始化时根据预计的元素数量分配足够的连续内存。
- Set和Map:改进哈希表的实现,减少哈希冲突,提高查找、插入和删除的速度。例如采用更高效的哈希函数和哈希表扩容策略。
- 生成器:优化生成器的状态管理和迭代器实现,减少状态切换的开销。例如,在生成器暂停和恢复时,更高效地保存和恢复上下文。
- 代码层面:
- 数组:避免在数组中间频繁插入和删除元素,尽量使用
push
和pop
操作。使用for
循环代替for...of
循环进行简单遍历,因为for
循环性能更好。例如:
let arr = [1, 2, 3]; // 避免 arr.splice(1, 0, 4); // 推荐 arr.push(4);
- Set和Map:尽量使用
add
、delete
等原生方法,避免自行实现复杂的查找和插入逻辑。提前预估元素数量,合理设置哈希表初始容量,减少扩容次数。例如:
let set = new Set(); let expectedCount = 100; // 提前设置合适的初始容量(假设引擎支持设置初始容量相关参数) // set = new Set(null, { initialCapacity: expectedCount }); for (let i = 0; i < expectedCount; i++) { set.add(i); }
- 生成器:简化生成值的逻辑,减少复杂计算。缓存生成的值,如果同一个值可能被多次使用。例如:
function* gen() { let cache = {}; for (let i = 0; i < 10; i++) { if (!cache[i]) { cache[i] = complexCalculation(i); } yield cache[i]; } } function complexCalculation(num) { // 复杂计算逻辑 return num * num; }
- 数组:避免在数组中间频繁插入和删除元素,尽量使用