前言:为什么要手写数组方法?
在日常开发中,我们会这样使用数组方法:
const numbers = [1, 2, 3, 4, 5];
// map:转换数组
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter:过滤数组
const evenNumbers = numbers.filter(n => n % 2 === 0);
console.log(evenNumbers); // [2, 4]
// reduce:累积计算
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15
这些代码看起来很简单,但你知道它们内部是如何工作的吗?如果让我们自己实现这些方法,又该怎么做呢?理解数组方法的内部实现不仅能帮助我们在面试中脱颖而出,更能让我们在实际开发中做出更明智的选择。
实现 forEach 方法
原生 forEach 的基本使用
const arr = [1, 2, 3];
arr.forEach((item, index, array) => {
console.log(item, index, array);
});
手写实现
Array.prototype.myForEach = function(callback, thisArg) {
// 1. 检查this是否合法
if (this == null) {
throw new TypeError('this为空,无法调用forEach!');
}
// 2. 检查callback是否是函数
if (typeof callback !== 'function') {
throw new TypeError(callback + ' 不是函数,无法调用forEach!');
}
// 3. 将this转换为对象(处理基本类型)
const thisObj = Object(this);
// 4. 获取数组长度(使用无符号右移,确保是正整数,防止长度为负数或非整数)
const len = thisObj.length >>> 0;
// 5. 使用 for 循环遍历数组
for (let k = 0; k < len; k++) {
callback.call(thisArg, thisObj[k], k, thisObj);
}
// 6. 返回undefined(forEach没有返回值)
return undefined;
};
实现 map 方法
原生的 map 的基本使用
const arr = [1, 2, 3];
const mapped = arr.map(x => x * 2);
console.log(mapped); // [2, 4, 6]
手写实现
Array.prototype.myMap = function (callback, thisArg) {
// 1. 检查this是否合法
if (this == null) {
throw new TypeError('this为空,无法调用forEach!');
}
// 2. 检查callback是否是函数
if (typeof callback !== 'function') {
throw new TypeError(callback + ' 不是函数,无法调用forEach!');
}
// 3. 将this转换为对象(处理基本类型)
const thisObj = Object(this);
// 4. 获取数组长度(使用无符号右移,确保是正整数,防止长度为负数或非整数)
const len = thisObj.length >>> 0;
// 5. 创建结果数组(指定长度提高性能)
const result = new Array(len);
// 6. 遍历处理
for (let k = 0; k < len; k++) {
// 调用回调函数,获取转换后的值
const mappedValue = callback.call(thisArg, thisObj[k], k, thisObj);
result[k] = mappedValue;
}
// 7. 返回新数组
return result;
};
实现 filter 方法
原生的 filter 的基本使用
const arr = [1, 2, 3];
const evens = arr.filter(n => n % 2 === 0);
console.log(evens); // [2]
手写实现
Array.prototype.myFilter = function (callback, thisArg) {
// 1. 检查this是否合法
if (this == null) {
throw new TypeError('this为空,无法调用forEach!');
}
// 2. 检查callback是否是函数
if (typeof callback !== 'function') {
throw new TypeError(callback + ' 不是函数,无法调用forEach!');
}
// 3. 将this转换为对象(处理基本类型)
const thisObj = Object(this);
// 4. 获取数组长度(使用无符号右移,确保是正整数,防止长度为负数或非整数)
const len = thisObj.length >>> 0;
// 5. 结果数组(初始为空)
const result = [];
// 6. 遍历数组
for (let k = 0; k < len; k++) {
// 调用回调函数判断是否保留
if (callback.call(thisArg, thisObj[k], k, thisObj)) {
result.push(thisObj[k]);
}
}
// 7. 返回新数组
return result;
};
实现 reduce 方法
原生的 reduce 的基本使用
const arr = [1, 2, 3];
const sum = arr.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 6
手写实现
Array.prototype.myReduce = function (callback, initialValue) {
// 1. 检查this是否合法
if (this == null) {
throw new TypeError('this为空,无法调用forEach!');
}
// 2. 检查callback是否是函数
if (typeof callback !== 'function') {
throw new TypeError(callback + ' 不是函数,无法调用forEach!');
}
// 3. 将this转换为对象(处理基本类型)
const thisObj = Object(this);
// 4. 获取数组长度(使用无符号右移,确保是正整数,防止长度为负数或非整数)
const len = thisObj.length >>> 0;
// 5. 处理空数组且没有初始值的情况
if (len === 0 && initialValue === undefined) {
throw new TypeError('数组为空且没有初始值,无法调用reduce!');
}
let accumulator;
let startIndex;
// 6. 确定初始值和起始索引
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = 0;
} else {
// 使用第一个元素作为初始值
accumulator = arr[0];
startIndex = 1;
}
// 7. 遍历数组
for (let i = startIndex; i < len; i++) {
accumulator = callback(accumulator, thisObj[i], i, thisObj);
}
// 8. 返回累加结果
return accumulator;
}
面试题:数组去重的方法
方法1:Set(ES6,最简单)
function uniqueBySet(arr) {
// 一行代码解决
return [...new Set(arr)];
}
- 时间复杂度: O(n)
- 空间复杂度: O(n)
- 最简单,代码最少,保持插入顺序,支持任何类型的数组
方法2:filter + indexOf
function uniqueByFilterIndexOf(arr) {
return arr.filter((item, index) => {
// indexOf返回第一个匹配项的索引
// 如果当前索引不是第一个匹配项,说明是重复的
return arr.indexOf(item) === index;
});
}
- 时间复杂度: O(n²) - 每次indexOf都需要遍历
- 空间复杂度: O(n)
- 兼容性好,保持顺序,但性能差
方法3:reduce + includes
function uniqueByReduceIncludes(arr) {
return arr.reduce((acc, current) => {
if (!acc.includes(current)) {
acc.push(current);
}
return acc;
}, []);
}
- 时间复杂度: O(n²) - includes内部也是遍历
- 空间复杂度: O(n)
- 函数式编程风格,保持顺序
方法4:排序后相邻比较
function uniqueBySort(arr) {
// 先复制数组,避免修改原数组
const sortedArr = [...arr].sort();
const result = [];
for (let i = 0; i < sortedArr.length; i++) {
// 第一个元素或与上一个元素不同
if (i === 0 || sortedArr[i] !== sortedArr[i - 1]) {
result.push(sortedArr[i]);
}
}
return result;
}
- 时间复杂度: O(n log n) - 排序的复杂度
- 空间复杂度: O(n)
- 会改变顺序,适合不在乎顺序的场景
方法5:Map实现(保持顺序)
function uniqueByMap(arr) {
const map = new Map();
const result = [];
for (const item of arr) {
if (!map.has(item)) {
map.set(item, true);
result.push(item);
}
}
return result;
}
- 时间复杂度: O(n)
- 空间复杂度: O(n)
- 保持顺序,支持任意类型,性能优秀
方法6:双重循环+splice(性能最差)
function uniqueByDoubleLoop(arr) {
// 复制数组,避免修改原数组
const result = [...arr];
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
if (result[i] === result[j]) {
result.splice(j, 1); // 删除重复元素
j--; // 调整索引
}
}
}
return result;
}
- 时间复杂度: O(n²) - 最坏情况
- 空间复杂度: O(1) - 原地修改
- 原地修改,不需要额外空间,但性能差,会修改原数组
面试题:数组扁平化的方法
方法1:flat实现
const arr = [1, [2, [3, [4, 5]]], 6];
// 无限深度扁平化
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5]
方法2:递归实现
function flatten(arr) {
let result = [];
for (let item of arr) {
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
}
return result;
}
方法3:reduce 实现
function flatten(arr) {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
方法4:迭代实现
function flatten(arr) {
const stack = [...arr];
const result = [];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
result.push(next);
}
}
return result.reverse();
}
方法5:扩展运算符实现
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
方法6:正则表达式替换
function flatten(arr) {
const str = JSON.stringify(arr);
// 移除所有方括号(除了最外层的)
const flatStr = str.replace(/(\[|\])/g, '');
return JSON.parse(`[${flatStr}]`);
}
扩展题:将字符串数组转成对象数组
假设有这样一个字符串数组:['a.b.c.d:1', 'a.b.e:2', 'x.m.n:3', 'x.y:4'],请你将它转出对象数组:
[
{
"a": {
"b": {
"c": {
"d": "1"
},
"e": "2"
}
}
},
{
"x": {
"m": {
"n": "3"
},
"y": "4"
}
}
]
解题思路
- 遍历数组
- 将数组中的元素先按冒号
:进行分割,左边是键,右边是值 - 对键进行遍历,构建嵌套数组
代码实现
Array.prototype.toNestedObjects = function () {
const map = new Map();
// 数组遍历
this.forEach(item => {
// 分割路径和值
const [path, value] = item.split(':');
const keys = path.split('.');
// 用第一个键作为根键
const rootKey = keys[0];
if (!map.has(rootKey)) {
map.set(rootKey, {});
}
// 获取根键对应的对象
let obj = map.get(rootKey);
// 构建嵌套对象
for (let i = 1; i < keys.length - 1; i++) {
obj[keys[i]] = obj[keys[i]] || {};
obj = obj[keys[i]];
}
// 设置叶子节点的值
obj[keys[keys.length - 1]] = value;
});
return Array.from(map, ([key, value]) => ({ [key]: value }));
};
结语
通过手写实现这些数组方法,不仅能让我们掌握它们的原理,更能根据实际需求选择或创造最适合的解决方案。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!