(一)数组常见需求 检验数组 不能使用 typeof 检验数组,因为它的结果只有 undefined、object、function、boolean、string、number、bigint、symbol。
1 2 typeof []; typeof null ;
推荐方法:
1 2 Array .isArray([]); Object .prototype.toString.call([]) === '[object Array]' ;
Array.isArray 是 ES5+ 标准,优先使用;Object.prototype.toString 是通用备选方案。
数组去重 1 2 3 4 5 6 7 8 9 10 const arr = [1 , 2 , 3 , 2 , 1 , 4 ];[...new Set (arr)]; arr.filter((item, idx ) => arr.indexOf(item) === idx); 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 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 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 ); arr.filter(item => item !== 2 ); for (let i = arr.length - 1 ; i >= 0 ; i--) { if (arr[i] < 3 ) arr.splice(i, 1 ); } 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(); [...arr].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)); [...new Set ([...a, ...b])]; a.filter(x => !b.includes(x)); 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 6 7 8 9 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' ); [...divs]; Array .from(divs); Array .prototype.slice.call(divs);
类数组的特征:有 length 属性、元素可通过索引访问(如 arguments、NodeList、HTMLCollection)。
数组方法分类总结 改变原数组 方法 作用 返回值 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 ); [1 , , 3 ]; [1 , , 3 ].map(x => x + 1 ); [1 , , 3 ].forEach(x => console .log(x));
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 Array .prototype.myForEach = function (cb ) { for (let i = 0 ; i < this .length; i++) { if (i in this ) cb(this [i], i, this ); } }; 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; }; 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; }; Array .prototype.myReduce = function (cb, init ) { let acc = init, i = 0 ; if (acc === undefined ) { 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; }; 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 ); parseInt ("0x3" ); parseInt ("0x3" , 16 ); parseInt ("3" , 2 ); parseInt (0x3 , 2 );
考点总结 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 ].sort((a, b ) => b - a); [1 , 10 , 2 ].sort(); [1 , 10 , 2 ].sort((a, b ) => a - b);
3. reduce 实现 map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Array .prototype.mapByReduce = function (cb ) { return this .reduce((acc, cur, idx, arr ) => { acc.push(cb(cur, idx, arr)); return acc; }, []); }; 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);
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; }, {});
8. 数组相关手写题汇总 题目 核心思路 拍平任意层级数组 递归 + reduce / 迭代 + 栈 数组去重 Set / filter+indexOf 实现 Promise.all 用数组收集结果 数组转树 Map 建立 id → node 映射 树转数组 广度优先队列 洗牌算法 Fisher-Yates 数组分块 slice + 循环 排序稳定性 稳定排序(sort)+ 自定义比较 实现 flatMap map + 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 ));
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 );
需要 fill(0) 的原因:Array(layer) 创建稀疏数组,map 不会遍历 empty 元素 。fill(0) 填充后再 map 才能执行回调。