JS 数组常见需求

(一)数组常见需求

检验数组

不能使用 typeof 检验数组,因为它的结果只有 undefinedobjectfunctionbooleanstringnumberbigintsymbol

1
2
typeof [];           // "object"
typeof null; // "object"(历史遗留 bug)

推荐方法:

1
2
Array.isArray([]);                                // true
Object.prototype.toString.call([]) === '[object Array]'; // true

Array.isArray 是 ES5+ 标准,优先使用;Object.prototype.toString 是通用备选方案。

数组去重

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

// Set(最简洁,推荐)
[...new Set(arr)]; // [1, 2, 3, 4]

// filter + indexOf(ES5)
arr.filter((item, idx) => arr.indexOf(item) === idx);

// reduce
arr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], []);

对象数组去重需要指定 key:

1
2
3
4
5
6
7
8
9
10
11
12
const users = [{ id: 1 }, { id: 2 }, { id: 1 }];
uniqueBy(users, 'id');

function uniqueBy(arr, key) {
const seen = new Set();
return arr.filter(item => {
const val = item[key];
if (seen.has(val)) return false;
seen.add(val);
return true;
});
}

数组扁平化

1
[1, [2, [3]]].flat(2);        // [1, 2, 3](Infinity 表示完全扁平)

手写实现

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
// 递归 + concat
function flatten(arr) {
return arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flatten(cur) : cur), []);
}

// 扩展运算符
function flatten(arr) {
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
return arr;
}

// 迭代器(适用于深层嵌套)
function flatten(arr) {
const stack = [...arr], result = [];
while (stack.length) {
const item = stack.shift();
Array.isArray(item) ? stack.unshift(...item) : result.push(item);
}
return result;
}

// 控制深度
function flatten(arr, depth = 1) {
return depth > 0
? arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flatten(cur, depth - 1) : cur), [])
: arr.slice();
}

删除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 按值删除(只删第一个)
const arr = [1, 2, 3, 4];
const idx = arr.indexOf(2);
if (idx > -1) arr.splice(idx, 1); // [1, 3, 4]

// 按值删除全部匹配
arr.filter(item => item !== 2); // 不改变原数组

// 按条件删除(原地)
for (let i = arr.length - 1; i >= 0; i--) {
if (arr[i] < 3) arr.splice(i, 1); // 倒序遍历避免 index 偏移
}

// 原地批量删除
let i = arr.length;
while (i--) { if (arr[i] < 3) arr.splice(i, 1); }

数组翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原地翻转
[1, 2, 3].reverse(); // [3, 2, 1]

// 不改变原数组
[...arr].reverse();

// 手写 reverse
function reverse(arr) {
let i = 0, j = arr.length - 1;
while (i < j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++; j--;
}
return arr;
}

交集 / 并集 / 差集

1
2
3
4
5
6
7
8
9
10
11
12
13
const a = [1, 2, 3, 4], b = [3, 4, 5, 6];

// 交集
a.filter(x => b.includes(x)); // [3, 4]

// 并集
[...new Set([...a, ...b])]; // [1, 2, 3, 4, 5, 6]

// 差集(A 有 B 无)
a.filter(x => !b.includes(x)); // [1, 2]

// 对称差
a.filter(x => !b.includes(x)).concat(b.filter(x => !a.includes(x)));

数组分块

1
2
3
4
5
6
7
8
9
function chunk(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}

chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]

数组乱序

1
2
3
4
5
6
7
8
9
// Fisher-Yates 洗牌
function shuffle(arr) {
const res = [...arr];
for (let i = res.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[res[i], res[j]] = [res[j], res[i]];
}
return res;
}

类数组(Array-like)转数组

1
2
3
4
5
6
const divs = document.querySelectorAll('div');  // NodeList(类数组)

// 方式对比
[...divs]; // 展开运算符
Array.from(divs); // Array.from(推荐)
Array.prototype.slice.call(divs); // ES5 hack

类数组的特征:有 length 属性、元素可通过索引访问(如 argumentsNodeListHTMLCollection)。

数组方法分类总结

改变原数组

方法作用返回值
push(val)尾部追加新 length
pop()尾部删除删除的元素
unshift(val)头部插入新 length
shift()头部删除删除的元素
splice(start, count, ...items)增删改删除的元素数组
reverse()翻转原数组
sort(fn)排序原数组
fill(val, start, end)填充原数组

不改变原数组

方法作用返回值
concat(arr)合并新数组
slice(start, end)截取新数组
join(sep)拼接为字符串字符串
indexOf(val) / lastIndexOf(val)查找索引数字
includes(val)是否存在布尔
forEach(fn)遍历undefined
map(fn)映射新数组
filter(fn)过滤新数组
reduce(fn, init)归并累积值
some(fn)任一满足布尔
every(fn)全部满足布尔
find(fn)查找第一个满足的元素元素 / undefined
findIndex(fn)查找第一个满足的索引数字
flat(depth)扁平化新数组
flatMap(fn)映射后扁平化一层新数组

稀疏数组

1
2
3
4
5
6
Array(3);                            // [empty × 3](稀疏)
[1, , 3]; // [1, empty, 3]

// empty 和 undefined 不同
[1, , 3].map(x => x + 1); // [2, empty, 4](empty 元素不执行回调)
[1, , 3].forEach(x => console.log(x)); // 只打印 1 和 3

forEach 中动态增删元素的面试题

题目

1
2
3
4
5
6
7
8
9
const arr = [1, 2, 3, 4, 5];

arr.forEach((item, index) => {
if (item === 3) {
arr.shift();
arr.push(6);
}
console.log(`index=${index}, item=${item}, arr=[${arr}]`);
});

问:输出是什么?

答案

1
2
3
4
5
index=0, item=1, arr=[1,2,3,4,5]
index=1, item=2, arr=[1,2,3,4,5]
index=2, item=3, arr=[1,2,3,4,5]
index=3, item=5, arr=[2,3,4,5,6]
index=4, item=6, arr=[2,3,4,5,6]

解析

forEach 在遍历开始前缓存 length(5),之后 index 从 0 递增到 4。

  • index=2 触发 shift() 删除 1,元素前移:[2,3,4,5]push(6)[2,3,4,5,6]
  • 原值 4(原 index=3)前移到 index=2,而 index=2 已过,因此 4 被永久跳过
  • index=3 读取新 arr[3] → 5(原 index=4 前移)
  • index=4 读取新 arr[4] → 6(push 追加)
操作影响
push追加的元素若落在缓存 length 范围内会被遍历
shift / splice删除前置元素导致后续元素前移,原位置元素被跳过
unshift插入后原元素后移,部分元素被重复访问

数组方法实现(手写)

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
45
46
47
48
49
50
51
52
53
// forEach
Array.prototype.myForEach = function(cb) {
for (let i = 0; i < this.length; i++) {
if (i in this) cb(this[i], i, this); // 跳过 empty
}
};

// map
Array.prototype.myMap = function(cb) {
const result = new Array(this.length);
for (let i = 0; i < this.length; i++) {
if (i in this) result[i] = cb(this[i], i, this);
}
return result;
};

// filter
Array.prototype.myFilter = function(cb) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this && cb(this[i], i, this)) result.push(this[i]);
}
return result;
};

// reduce
Array.prototype.myReduce = function(cb, init) {
let acc = init, i = 0;
if (acc === undefined) {
// 找第一个非 empty 元素作为初始值
while (i < this.length && !(i in this)) i++;
if (i >= this.length) throw new TypeError('Reduce of empty array with no initial value');
acc = this[i++];
}
for (; i < this.length; i++) {
if (i in this) acc = cb(acc, this[i], i, this);
}
return acc;
};

// flat
function myFlat(arr, depth = 1) {
if (depth === 0) return arr.slice();
let result = [];
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...myFlat(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}

(二)面试题

1. ["0x1", "0x2", "0x3"].map(parseInt)

结果:[1, NaN, 0]

分析步骤

第一步:理解 map 传给回调函数的参数

1
arr.map((currentValue, index, array) => { ... })

map 始终传 3 个参数给回调函数:当前元素、索引、原数组。

因此 ["0x1", "0x2", "0x3"].map(parseInt) 等价于:

1
2
3
4
5
[
parseInt("0x1", 0, ["0x1", "0x2", "0x3"]),
parseInt("0x2", 1, ["0x1", "0x2", "0x3"]),
parseInt("0x3", 2, ["0x1", "0x2", "0x3"]),
]

parseInt 只接收前两个参数(string, radix),第三个参数 arr 被忽略。所以等价于:

1
2
3
parseInt("0x1", 0);
parseInt("0x2", 1);
parseInt("0x3", 2);

第二步:理解 parseInt(string, radix) 的行为

1
2
3
4
5
parseInt 的 radix 取值范围:2 ~ 36
若 radix 为 0、undefined 或未传:
→ 自动识别:字符串以 "0x" 开头视为 16 进制,否则视为 10 进制
若 radix 在 2~36 范围内:
→ 按指定进制解析,只取该进制下有效字符,遇到无效字符停止

第三步:逐项计算

1
2
3
4
5
parseInt("0x1", 0)
→ radix = 0,自动识别
→ "0x1" 以 "0x" 开头,浏览器认为这是 16 进制表示法
→ 16 进制的 "0x1" = 1(十进制)
→ 结果:1
1
2
3
4
5
parseInt("0x2", 1)
→ radix = 1(显式传入)
→ 1 进制不存在(有效进制 2~36)
→ 任何不合法进制都返回 NaN
→ 结果:NaN
1
2
3
4
5
6
7
8
parseInt("0x3", 2)
→ radix = 2,按二进制解析
→ 二进制有效字符:0、1
→ "0x3" 从左到右逐个字符扫描:
"0" → 有效二进制位
"x" → 无效二进制位,停止解析
→ 只解析到 "0",二进制 "0" = 0(十进制)
→ 结果:0

常见追问

如果直接写 parseInt("0x3", 2) 为什么结果是 0 而不是 NaN

1
2
3
4
5
6
7
8
parseInt 不是将整个字符串视为二进制,而是逐个字符扫描
直到遇到第一个无效字符停止。

"0x3" 中:
"0" → 二进制有效字符 ✓
"x" → 二进制无效字符 ✗(停止)

所以只解析了 "0",二进制 "0" = 0。

对比:

1
2
3
4
5
parseInt("0x3", 2);         // 0(只解析到 "0")
parseInt("0x3"); // 3(radix 省略,自动识别)
parseInt("0x3", 16); // 3(16 进制下 "0x3" → "3")
parseInt("3", 2); // NaN(二进制中没有 "3")
parseInt(0x3, 2); // NaN(0x3 先被 JS 引擎解析为数字 3,再转为字符串 "3",二进制无 "3")

考点总结

1
2
3
4
5
6
├── map 传几个参数给回调(3 个:value, index, arr)
├── parseInt 接收几个参数(2 个:string, radix)
├── radix = 0 或 undefined 时自动识别进制("0x" 开头 = 16 进制)
├── radix 不合法(< 2 或 > 36)时返回 NaN
├── radix 合法时逐个字符扫描,遇非法字符停止
└── 批处理时 index 被当作 radix 传入是常见陷阱

2. array.sort() 的比较返回值

1
2
3
4
5
6
[1, 2, 3].sort((a, b) => a - b);   // 升序 [1, 2, 3]
[1, 2, 3].sort((a, b) => b - a); // 降序 [3, 2, 1]

// 注意:sort 默认转字符串后按 Unicode 排序
[1, 10, 2].sort(); // [1, 10, 2](字符串比较 "10" < "2")
[1, 10, 2].sort((a, b) => a - b); // [1, 2, 10]

3. reduce 实现 map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// reduce 实现 map
Array.prototype.mapByReduce = function(cb) {
return this.reduce((acc, cur, idx, arr) => {
acc.push(cb(cur, idx, arr));
return acc;
}, []);
};

// reduce 实现 filter
Array.prototype.filterByReduce = function(cb) {
return this.reduce((acc, cur, idx, arr) => {
if (cb(cur, idx, arr)) acc.push(cur);
return acc;
}, []);
};

4. 数组转树与树转数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 扁平列表 → 树
const list = [
{ id: 1, pid: null, name: 'A' },
{ id: 2, pid: 1, name: 'A-1' },
{ id: 3, pid: 1, name: 'A-2' },
{ id: 4, pid: 2, name: 'A-1-1' },
];

function listToTree(list) {
const map = {}, roots = [];
list.forEach(item => { map[item.id] = { ...item, children: [] }; });
list.forEach(item => {
if (item.pid && map[item.pid]) {
map[item.pid].children.push(map[item.id]);
} else {
roots.push(map[item.id]);
}
});
return roots;
}

5. 数组拍平与去重结合

1
2
3
4
// 多维数组 → 排序 → 去重 → 升序
const nested = [[3, 1], [2, [1, 4]], 3];
[...new Set(nested.flat(Infinity))].sort((a, b) => a - b);
// [1, 2, 3, 4]

6. 最大 / 最小值

1
2
3
4
5
Math.max(...arr);    // 注意:数组过大可能栈溢出
Math.min(...arr);

// 安全写法
arr.reduce((max, cur) => Math.max(max, cur));

7. 统计出现次数

1
2
3
4
5
6
7
const items = ['a', 'b', 'a', 'c', 'b', 'a'];

items.reduce((acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {});
// { a: 3, b: 2, c: 1 }

8. 数组相关手写题汇总

题目核心思路
拍平任意层级数组递归 + reduce / 迭代 + 栈
数组去重Set / filter+indexOf
实现 Promise.all用数组收集结果
数组转树Map 建立 id → node 映射
树转数组广度优先队列
洗牌算法Fisher-Yates
数组分块slice + 循环
排序稳定性稳定排序(sort)+ 自定义比较
实现 flatMapmap + concat

9. array.sort() 进阶

1
2
3
4
5
var arr = [-1, 1, 3, 4];
arr.sort((a, b) => Math.abs(a - 2) - Math.abs(b - 2));
// 按离 2 的距离升序
// |-1-2|=3, |1-2|=1, |3-2|=1, |4-2|=2
// 排序结果:[1, 3, -1, 4]

10. 递归生成多维数组

1
2
3
4
5
6
7
8
9
function fun(...dims) {
const [layer, ...rest] = dims;
return rest.length
? Array(layer).fill(0).map(_ => fun(...rest))
: Array(layer);
}

fun(2, 3, 4);
// 生成 2×3×4 的三维数组

需要 fill(0) 的原因:Array(layer) 创建稀疏数组,map 不会遍历 empty 元素fill(0) 填充后再 map 才能执行回调。