《JavaScript数组深度剖析:从基础到高阶》

104 阅读8分钟

前言

在JS中,数组是一个十分重要的知识点,不论是备战面试的八股中,还是各式各样的算法里,掌握数组的都是很必要的,本文我们来深度剖析数组这个知识点,从各个角度去讲解数组的特性与本质,话不多说,就让我们直接开始吧!

数组是对象

在 JavaScript 中,实际上是没有单独的数组的,我们所说的数组本质上是一种特殊的对象。虽然它用起来可以像其它语言使用数组一样,比如通过arr[索引]的方式来使用,但是你需要清楚的是,在JS中,它的底层是实现是基于对象的键值对结构。

正是因为JS中的数组是特殊的对象,所以相对于其它语言来说,JS的数组有着许多独特的特性!这也是我们接下来要探讨的!


1.数组可以定义容量 也可以扩容

在大多数强类型语言(如 Java、C++)中,数组的长度是固定的,一旦声明就无法改变。但在 JavaScript 中,数组是动态的,可以随时扩展或缩减:

let arr = [1, 2, 3]; // 初始长度为 3  
arr[5] = 6;          // 动态扩容,此时数组长度为 6,中间空位填充 undefined  
console.log(arr);    // [1, 2, 3, undefined, undefined, 6]  

在JS中,数组既可以自定义长度,也可以动态变更长度!

2.数组不限类型

传统语言的数组,必须指定为某一具体类型,但在js中,由于数组就实际上是键值对,所以,无所谓类型,同一个数组中可以存储任意类型的元素

JavaScript 数组的本质:

// 数组在底层类似于这样的对象
const arrayLike = {
  "0": "first",
  "1": 42,
  "2": { name: "object" },
  "3": [1, 2, 3],
  length: 4
};

混合类型数组示例:

const mixedArray = [
            42,                       // Number
            "Hello",                  // String
            { name: "Object" },       // Object
            [1, 2, 3],                // Array
            true,                     // Boolean
            null,                     // Null
            undefined,                // Undefined
            function() {              // Function
              return "I'm a function";
            },
            new Date(),               // Date
            /regex/,                  // RegExp
            Symbol("foo")             // Symbol
          ];
          
          console.log(mixedArray.length);  // 11

你看吧,这个数组mixedArray存储了几乎所有的数据类型!

数组可以当作各种数据结构--链表、队列、栈

由于 JavaScript 数组的动态性和灵活性,它可以模拟多种数据结构:

栈(Stack) :使用 push() 和 pop() 实现后进先出(LIFO)。

// 使用数组模拟栈
const stack = [];

// 入栈 - 使用push()方法
stack.push('A');
stack.push('B');
stack.push('C');
console.log(stack); // ['A', 'B', 'C']

// 出栈 - 使用pop()方法
const lastItem = stack.pop();
console.log(lastItem); // 'C'
console.log(stack); // ['A', 'B']

// 查看栈顶元素(不弹出)
const topItem = stack[stack.length - 1];
console.log(topItem); // 'B'

队列(Queue) :使用 push() 和 shift() 实现先进先出(FIFO)。

// 使用数组模拟队列
const queue = [];

// 入队 - 使用push()方法
queue.push('A');
queue.push('B');
queue.push('C');
console.log(queue); // ['A', 'B', 'C']

// 出队 - 使用shift()方法
const firstItem = queue.shift();
console.log(firstItem); // 'A'
console.log(queue); // ['B', 'C']

// 查看队首元素(不出队)
const frontItem = queue[0];
console.log(frontItem); // 'B'

链表(Linked List) :虽然 JavaScript 没有内置链表,但可以通过对象和数组模拟其行为。

// 定义链表节点
class ListNode {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

// 创建链表
const node1 = new ListNode('A');
const node2 = new ListNode('B');
const node3 = new ListNode('C');

// 连接节点
node1.next = node2;
node2.next = node3;

// 遍历链表
function traverseList(head) {
  let current = head;
  while (current !== null) {
    console.log(current.value);
    current = current.next;
  }
}

traverseList(node1); // 输出: A B C

// 在链表末尾添加节点
function appendNode(head, value) {
  const newNode = new ListNode(value);
  if (!head) return newNode;
  
  let current = head;
  while (current.next !== null) {
    current = current.next;
  }
  current.next = newNode;
  return head;
}

// 使用数组初始化链表
function createListFromArray(arr) {
  if (!arr.length) return null;
  
  const head = new ListNode(arr[0]);
  let current = head;
  
  for (let i = 1; i < arr.length; i++) {
    current.next = new ListNode(arr[i]);
    current = current.next;
  }
  
  return head;
}

const myList = createListFromArray(['X', 'Y', 'Z']);
traverseList(myList); // 输出: X Y Z

JS就是那么神奇!它没有自己独立的栈、队列等数据结构,妙用数组,可以既当栈又当队列!


数组的声明方法

JavaScript 提供了多种声明数组的方式:

  1. 字面量方式(最常用):

    let arr1 = [1, 2, 3];  
    
  2. 构造函数方式

    let arr2 = new Array(1, 2, 3); // [1, 2, 3]   
    
  3. ES6 的 Array.of() (解决构造函数歧义):

    let arr4 = Array.of(3); // [3],而不是长度为 3 的空数组  
    

    这里提一嘴:需要注意new Array()Array.of()的区分

  • new Array(3)是声明一个长度为3的数组[undefied,undefied,undefied]
  • Array.of(3)是声明一个长度为1的数组[3]
  1. ES6 的 Array.from() (将类数组或可迭代对象转为数组):

    let arr5 = Array.from("hello"); // ['h', 'e', 'l', 'l', 'o']  
    // 传入第二个参数:回调函数
    console.log(Array.from(new Array(26),(val,index) => String.fromCodePoint(65 + index)))
    

    注意Array.from可以接受第二个参数mapFn用来调用数组每个元素的函数,如果提供,每个将要添加到数组中的值首先会传递给该函数,然后将 mapFn 的返回值增加到数组中


数组的遍历方式

JavaScript 提供了多种遍历数组的方法,每种方法都有其适用场景和特点。下面我将详细补充各种遍历方式的特性、优缺点和使用场景。

1. 传统 for 循环 (for(let i...))

const arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
    console.log(`索引 ${i} 的值是 ${arr[i]}`);
}

特点:

最基础的循环方式

通过索引访问数组元素

性能最优(对CPU最友好)

2. for...in 循环

const arr = [10, 20, 30];
Array.prototype.customProp = '不要遍历我'; // 在原型上添加属性

for (let index in arr) {
    console.log(`索引 ${index} 的值是 ${arr[index]}`);
    // 会输出: 0 10, 1 20, 2 30, 还会输出 customProp '不要遍历我'
}

//也可以这样用
const arr = [1,2,3];
for(const [index,value] of arr.entries()) {
    // entries 返回可迭代的对象,每一项都是数组,第一项是key,第二项是值
    console.log(index,value);
}

特点:

遍历对象的可枚举属性(包括原型链上的属性

这点很重要,只有for...in这一种遍历方式会遍历原型链上的数据

可以配合数组的迭代器arr.entries()来使用

性能比传统for循环差

3. for...of 循环 (ES6)

const arr = [10, 20, 30];
for (let value of arr) {
    console.log(value); // 10, 20, 30
}

特点:

直接遍历数组的值而非索引

语法简洁明了

性能略低于传统for循环

4. forEach 方法

const arr = [10, 20, 30];
arr.forEach((value, index, array) => {
    console.log(`索引 ${index} 的值是 ${value}`);
    // 第三个参数 array 是原始数组的引用
});

const names = Array.of('Alice','Bob','Charlie','David');
names.forEach(name => {
    if(name === 'Charlie') {
        console.log('Charlie is here,stop...')
        return;
    }
    console.log('Processing' + name);
})
//输出结果为:
......
Charlie is here,stop...
ProcessingDavid

特点:

高阶函数,接收回调函数

提供值、索引和原数组三个参数

不能使用 break 或 continue,无法中途跳出循环(除非抛出异常)

这个forEach比较特别,我们需要注意它的特点:forEach 没有办法像 for 或 while 那样用 break 或 return 来中断整个循环。这是因为 forEach 的回调函数里的 return 只会结束当前这一次的回调(即跳出当前这一次的箭头函数),不会终止整个 forEach 循环。

如果上面的代码是这样写的:

if(name === 'Charlie') {
        console.log('Charlie is here,stop...')
        return;
        console.log("hello")
    }

它的回调函数里的 return 只会结束当前这一次的回调(不会执行console.log("hello")),但是forEach 还是会继续对下一个元素('David')执行回调。

5. 其他遍历方法补充

map (返回新数组)

const doubled = arr.map(value => value * 2);

filter (返回过滤后的新数组)

const evens = arr.filter(value => value % 2 === 0);

reduce (累积计算)

const sum = arr.reduce((acc, value) => acc + value, 0);

拔高部分--稀疏数组

接下来介绍关于数组的高阶知识,在面试中,关于数组的知识,可能只有2%的高手才能达到这个高度哟

稀疏数组

观察下面的代码,请回答会输出什么?

const arr = new Array(5);  // 指定了大小,并未使用fill指定元素
console.log(arr) 

答: 在浏览器的环境中,会输出以下:

image.png

在node的环境中,会输出以下:

image.png

原因解释:

  • new Array(5) 创建了一个长度为 5的数组,但没有实际赋值,每个位置都是“empty slot”(空槽),不是 undefined,而是“没有值”。
  • 这种数组叫做“稀疏数组”(sparse array),它有长度,但每个元素都还没被初始化。
  • 所以 console.log(arr) 显示的是 5 个 empty(空槽),而不是 [undefined, undefined, undefined, undefined, undefined]。

稀疏数组有什么意义?

稀疏数组(Sparse Array)指的是数组长度大于实际存储的元素个数,中间有“空槽”(empty slot),即有些索引没有值。比如:

const arr = new Array(5); // [ <5 empty items> ]
arr[2] = 'hello';         // [ <2 empty items>, 'hello', <2 empty items> ]

它的意义:

  1. 节省内存(理论上)

如果你只需要在很大的索引范围内存储少量数据,稀疏数组可以避免为每个索引都分配空间(但 JS 引擎实现未必真的节省很多内存)。

  1. 模拟“稀疏矩阵”或“稀疏表”

在某些算法或数据结构中,数据分布很稀疏(比如棋盘、地图、矩阵等),用稀疏数组可以只存有用的数据,未赋值的地方就是“空”。

  1. 兼容性和特殊需求

有些第三方库、API、老代码会用稀疏数组来表示“未定义”或“未赋值”的状态。

  1. 性能测试/边界测试

有时为了测试数组方法在稀疏数组上的表现(比如 forEach、map、filter 等),会特意构造稀疏数组。

注意:

  • 稀疏数组的“空槽”不是 undefined,而是“没有值”。
  • 很多数组方法会跳过空槽(如 forEach、map、filter),但 for...in、Object.keys 会遍历有值的索引。
  • 稀疏数组的意义主要在于节省空间、模拟稀疏数据结构、兼容特殊需求。但在实际开发中,除非有特殊需求,否则更推荐用对象或 Map 来存储稀疏数据。

总结

JavaScript 数组是一种灵活且强大的数据结构,它既是对象,又能模拟多种数据结构。掌握其声明方式、遍历方法以及底层特性,能够帮助我们在实际开发中更高效地使用它。希望本文对你理解 JavaScript 数组有所帮助! 🚀