欢迎光临
我们一直在努力

手写JavaScript数组核心方法

手写JavaScript数组核心方法

  • 前言:为什么要手写数组方法?
  • 实现 forEach 方法
    • 原生 forEach 的基本使用
    • 手写实现
  • 实现 map 方法
    • 原生的 map 的基本使用
    • 手写实现
  • 实现 filter 方法
    • 原生的 filter 的基本使用
    • 手写实现
  • 实现 reduce 方法
    • 原生的 reduce 的基本使用
    • 手写实现
  • 面试题:数组去重的方法
    • 方法1:Set(ES6,最简单)
    • 方法2:filter + indexOf
    • 方法3:reduce + includes
    • 方法4:排序后相邻比较
    • 方法5:Map实现(保持顺序)
    • 方法6:双重循环+splice(性能最差)
  • 面试题:数组扁平化的方法
    • 方法1:flat实现
    • 方法2:递归实现
    • 方法3:reduce 实现
    • 方法4:迭代实现
    • 方法5:扩展运算符实现
    • 方法6:正则表达式替换
  • 扩展题:将字符串数组转成对象数组
    • 解题思路
    • 代码实现
  • 结语

前言:为什么要手写数组方法?

在日常开发中,我们会这样使用数组方法:

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 }));
    };

    结语

    通过手写实现这些数组方法,不仅能让我们掌握它们的原理,更能根据实际需求选择或创造最适合的解决方案。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

    赞(0)
    未经允许不得转载:171主机测评 » 手写JavaScript数组核心方法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址