前言

刷题时为数组的 pushpopshiftunshift 等方法的区别而困惑?

为数组容器特性抓耳挠腮,时不时翻看教程文档,没有形成自己的体系?
看 JavaScript 算法题解,对于 ES6 特性比较陌生,没有头绪?

本文根据博主自身的刷题经验,系统总结了数组容器常见的实例方法,涵盖栈与队列操作、数组与字符串互转、双指针技巧、自定义排序以及遍历方法等核心场景。

读完本文,你将能够:

  • 熟练运用 push / pop / unshift / shift 实现栈和队列;
  • 分清 joinsplit 的用法与区别;
  • 掌握双指针与自定义排序的实战技巧;
  • 理解 mapforEach 的适用场景。

栈和队列的经典操作

学完数据结构后,我们都知道栈和队列的基本特性。

如栈是后进先出(LIFO),队列是先进先出(FIFO)。

但是落实到不同的编程语言中,对于栈和队列的基本操作可能就不尽相同。

那么在原生 JavaScript 中,有没有方法能实现栈和队列的基本操作呢?

答案是有的。

栈的基本操作

在介绍栈的基本操作前,需要先分别介绍数组的 push 方法和 pop 方法:

push 方法:数组末尾添加元素

push 方法可以在数组末尾添加元素,

其中要注意的是,push 方法可以添加多个元素。

返回的参数是数组添加元素后,数组的长度。

const arr = [];
arr.push(1); // 1(返回添加元素后数组长度)
arr; // [1]
// 可以在数组末尾添加多个元素
arr.push(true, {}); // 3(返回添加元素后数组长度)
arr; // [1, true, {}]

以下是博主自己总结的 push 方法伪代码,若看不懂可以跳过

// 数组结构定义
const Array = function () {
    this.array = [];
    this.element = null;
};

/**
 * @return {number}
 */
// 返回数组长度
Array.prototype.size = function () {
    return this.array.length;
};

/**
 * @param {element} element
 * @return {number}
 */
// 函数括号内的 ... 表示 rest 参数,用于获取函数的多余参数,结果存放在数组中
Array.prototype.push = function (...element) {
    // for ... of 循环遍历所有数组元素
    for (const elem of element) {
        this.array.push(elem);    // 在数组末尾添加元素
    }
    return this.size();    // 返回添加新元素后的数组长度
};

pop 方法:数组末尾删除元素

pop 方法用于删除数组的最后一个元素,并返回该元素。

const arr = ['a', 'b', 'c'];
arr.pop(); // 'c'(返回弹出的元素)
arr; // ['a', 'b']

其中要注意,若数组为空,对数组的 pop 操作会返回 undefined

[].pop() // undefined

push 方法和 pop 方法的结合,构成了后进先出(LIFO)的栈结构:

const arr = [];
arr.push(1, 2); // 2(添加元素后数组长度)
arr; // [1, 2]
arr.push(3); // 3(添加元素后数组长度)
arr; // [1, 2, 3]
arr.pop(); // 3(弹出的元素)
arr // [1, 2]

队列的基本操作

上述 pushpop 方法,针对的都是数组末尾增删元素的场景。

下文则针对数组起始位置增删元素的场景:

unshift 方法:数组首位的 "push 操作"

unshift 方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。

类比数组的 push 方法,

相当于 push 方法在数组末尾添加元素,unshift 方法在数组开头添加元素

二者互为关联。

const arr = ['a', 'b', 'c'];
arr.unshift('x'); // 4
arr; // ['x', 'a', 'b', 'c'](数组开头添加元素)
arr.push('y');  // 5
arr; // ['x', 'a', 'b', 'c', 'd'](数组末尾添加元素)

同样,unshift 方法也能添加多个元素。

const arr = ['a', 'b', 'c'];
arr.unshift('x', 'y'); // 5
arr; // ['x', 'y', 'a', 'b', 'c'](数组开头添加多个元素)
arr.push('m', 'n');  // 7
arr; // ['x', 'y', 'a', 'b', 'c', 'm', 'n'](数组末尾添加多个元素)

shift 方法:数组首位的 "pop 操作"

shift 方法用于删除数组的第一个元素,并返回该元素。

类比数组的 pop 方法,

相当于 pop 方法在数组末尾删除元素,shift 方法在数组开头删除元素

二者互为关联。

const arr = ['a', 'b', 'c'];
arr.shift(); // 'a'(开头弹出的元素)
arr; // ['b', 'c']
arr.pop(); // 'c'(末尾弹出的元素)
arr; // ['b']

push 方法和 shift 方法的结合,构成了先进先出(FIFO)的队列结构:

const arr = [];
arr.push(1, 2); // 2
arr; // [1, 2]
arr.push(3); // 3
arr; // [1, 2, 3]
arr.shift(); // 1(弹出的元素)
arr // [2, 3]

对于以上实例方法是否改变原数组,博主是这么理解的:

在栈和队列中的基本操作中,无论是弹入还是弹出元素,均会影响栈和队列容器内的元素。

所以对于 push / pop / unshift / shift 方法,一定会改变原数组

以上只展示数组中有关栈和队列的基本操作。

因为本文重点在于介绍 JavaScript 的数组容器特性。

更为硬核的数据结构和算法知识待更新。

数组和字符串的爱恨情仇

有时候刷题,需要运用到数组和字符串互相转换的场景。

相信大伙经常搞不清楚数组转字符串或者字符串转数组分别用到什么实例方法?

下文直接给出总结:

join => 拼接 => (数组)拼接字符串

split => 分割 => 分割字符串(为数组)

数组转字符串

join 方法用于将所有数组成员拼接成字符串。

用通俗易懂的语言理解,就是将数组中的逗号(,)替换成指定分隔符,并将数组转换成字符串。

const arr = [1, 2, 3, 4];
arr.join(' | ');	// "1 | 2 | 3 | 4"(逗号替换成分隔符(' | '))
// 不提供参数,则默认用逗号拼接成字符串
arr.join();				// "1, 2, 3, 4"

若数组成员是 undefinednull 等空位,自动转成空字符串。

[undefined, null].join('#');    // '#'

undefined => 空字符串

, => "#"

null => 空字符串

以上三个字符串做拼接

同样这是博主总结的 join 方法伪代码,若觉得复杂可以直接跳过。

/**
 * @param {string} str
 * @return {string}
*/
Array.prototype.join = function (str = ',') {
    let string = '';
    for (let i = 0; i < this.size() - 1; i++) {
        if (this.array[i] === undefined || this.array[i] === null) {
            // 若数组成员是 undefined 或 null 等空位,则自动转换成空字符串
            string += '' + str;
        } else {
            // 数组成员转换为字符串形式后拼接
            string += this.array[i].toString() + str;
        }
    }
    if (this.array[this.size() - 1] === undefined || this.array[this.size() - 1] === null) {
        string += '';
    } else {
        string += this.array[this.size() - 1];
    }
    return string;
};

字符串转数组

split 方法用于分割字符串。

该方法就是 join 的逆向了。

const str = 'a|b|c';
str.split('|');	// ["a", "b", "c"]
// 参数为空字符串,则数组成员为字符串每一个字符
str.split('');	// ["a", "|", "b", "|", "c"]
// 省略参数,则数组唯一成员是原字符串
str.split();		// ["a|b|c"]

巧妙的双指针

在刷题时,经常会运用到双指针技巧。如颠倒排列数组元素。

这个方法如果手撕相对比较简单,如:

/**
 * @return {element[]}
*/
Array.prototype.reverse = function () {
    let i = 0;
    let j = this.size() - 1;
    for ( ; i < j; i++, j--) {
        let temp = this.array[i];
        this.array[i] = this.array[j];
        this.array[j] = temp;
    }
    return this.array;
};

但有时如果想图省事,想直接运用封装好的操作该如何做?

reverse 方法提供了这个操作。

const arr = ["a", "b", "c"];
arr.reverse();
arr; // ["c", "b", "a"]

如何自定义排序?

对于单调递增、单调递减场景的排序

const nums = [1, 3, 2, 5, 4];
nums.sort((a, b) => a - b);
nums;    // [1, 2, 3, 4, 5](递增)
const nums = [1, 3, 2, 5, 4];
nums.sort((a, b) => b - a);
nums;    // [5, 4, 3, 2, 1](递减)

除了单调递增、单调递减场景的,博主还整理了其余场景下的自定义排序

根据绝对值递减排序

nums = [2,-3,-1,5,-4];
nums.sort((a, b) => Math.abs(b) - Math.abs(a));
nums;    // [ 5, -4, -3, 2, -1 ]

对二维数组自定义排序

nums = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]];
nums.sort((a, b) => {
    if (a[0] !== b[0]) {
        // 对 二维数组第一位 递减排序
        return b[0] - a[0];
    } else {
        // 在 二维数组第一位 相同的情况下,对 二维数组第二位 递增排序
        return a[1] - b[1];
    }
});
nums;    // [[7, 0], [7, 1], [6, 1], [5, 0], [5, 2], [4, 4]]

遍历数组独有实例方法

在常规从头到尾遍历一个数组时,大伙通常会用

const nums = [1, 2, 3, 4, 5];
for (let i = 0; i < nums.length; i ++) {
    // 处理逻辑
}

但是这么写,写久了容易繁琐。特别是如果要完整遍历一次数组时。

这时候可以用原生 JavaScript 中独有的遍历数组方法:mapforEach

map 方法

其中,map 方法传入的参数前两位分别是:当前成员、当前位置

这两个参数都可以根据实际情况省略:如保留第一个、省略第二个或两个参数都省略

但如果保留了第二个参数,则第一个参数也要保留。

const nums = [1, 2, 3, 4, 5];
nums.map((elem) => elem + 1);    // [2, 3, 4, 5, 6](返回新数组)
nums;    // [1, 2, 3, 4, 5]
const nums = [1, 2, 3, 4, 5];
nums.map((elem, index) => elem * index);    // [0, 2, 6, 12, 20](返回新数组)
nums;    // [1, 2, 3, 4, 5]

elem => 当前成员 => nums[i]

index => 当前位置 => i

forEach 方法

forEach 方法和 map 方法类似

唯一的区别在于仅仅在遍历元素的过程中操作数据,不返回新数组。

const nums = [1, 2, 3, 4, 5];
nums.forEach((elem, index) => elem * index);    // 不返回新数组,仅仅操作数据
nums;    // [1, 2, 3, 4, 5]

等价于

const nums = [1, 2, 3, 4, 5];
const result = [...nums];    // 将原数组复制到 result 数组里
for (let i = 0; i < nums.length; i ++) {
    result[i] = nums[i] * i;    // 操作原数组数据
}
nums;    // [1, 2, 3, 4, 5]

一句话总结:

在遍历数组时,

操作数据的同时返回新数组 => map

单纯用于操作数据 => forEach

但是要注意,这种方法只适用于从头到尾的完整遍历。

如果能意识到是完整遍历数组的场景,则优先考虑 map / forEach 方法。

结语

本文是博主在刷题过程中,针对 JavaScript 数组容器特性的系统性总结。从栈和队列的基本操作,到数组与字符串的相互转换,再到双指针、自定义排序和遍历方法,逐一梳理了最常用的实例方法及其适用场景。

不过,再多的总结也需要不断的应用——知识终究要靠实践才能真正内化。如遇到「先进先出」场景时优先想到 push + shift,遇到「需要返回新数组」时优先使用 map 而非 forEach。只有反复练习,才能将这些技巧变成肌肉记忆。

实例方法总结

实例方法改变原数组?返回参数
push✅ 改变新数组的长度
pop✅ 改变被删除的最后一个元素
unshift✅ 改变新数组的长度

shift

✅ 改变被删除的第一个元素
join❌ 不改变拼接后的字符串
split❌ 不改变(字符串方法)分割后的新数组
reverse✅ 改变改变后的原数组
sort✅ 改变排序后的原数组
map❌ 不改变回调处理后的新数组
forEach❌ 不改变undefined(仅遍历操作)

参考文章

Array 对象 - JavaScript 教程 - 网道

更多推荐