📚JS 数组精讲:从薛定谔的 undefined 到 entries 的妙用

173 阅读10分钟

前言

数组是 JavaScript 中最基础、最常用的数据结构之一。无论是处理列表、集合,还是进行数据转换、聚合统计,数组方法都扮演着非常重要的角色。

然而,初学者在使用数组时常常会遇到一些“意料之外”的现象,比如:

  • 用 new Array(5) 创建的数组为什么不能遍历?
  • arr[0] 是 undefined,但它真的是“空”的吗?
  • 如何在 for...of 中拿到数组的索引?
  • mapfilterreduce 到底有什么区别?

这些问题的背后,其实隐藏着 JavaScript 数组的底层机制和一些容易被忽略的细节。本文将带你从基础出发,深入理解 JavaScript 数组的工作原理,掌握常用的数组方法,并通过通俗易懂的方式解释它们的使用场景和注意事项。

无论你是刚入门的开发者,还是希望加深理解的进阶者,这篇文章都将为你提供清晰、系统的知识梳理。

new Array()

const arr = new Array(5); 
console.log(arr);

会输出什么?undefined??

我们来实践一下:

image.png

empty? 这是为何?

原因是:new Array(5) 会创建一个长度为 5 的空数组,但是数组中的元素都没有被显式赋值,是空槽

注意:虽然 JavaScript 的数组是动态的(可以随时 push、pop、扩展),但底层的 V8 引擎为了性能优化,在某些情况下会像 C++ 那样,为数组预先分配固定大小的内存空间

  • 比如当你使用 new Array(1000) 时,V8 会一次性分配足够大的内存空间来容纳 1000 个元素。
  • 这种方式可以提高访问和操作效率,避免频繁重新分配内存。

但这只是内部优化机制,不影响 JavaScript 的动态数组语义。


再来遍历一下:

let arr = new Array(5);
for (let key in arr) {
  console.log(key); // 不会输出任何内容
}

不能遍历。

是因为此时 new Array(5)是一个类数组,它有.length属性,但不能作为真正数组来使用。

当使用 new Array(5) 创建一个没有初始化元素的数组时,数组中的每个位置是 “empty” 槽,而不是 undefined。这些“empty”槽不会被 for...inforEachmap 等方法处理,因为它们不是真正的属性

Array 方法的使用

实例方法

Array.prototype.fill()

  1. 定义:它用于用一个固定值填充一个数组中从起始索引到终止索引范围内的全部元素

    如果没有指定起始和结束索引,则整个数组都会被填充。

  2. 基本用法

array.fill(value, start, end) // (start,end]
  • value: 用来填充数组的值。
  • start (可选): 开始填充的位置,默认是 0
  • end (可选): 结束填充的位置(不包括该位置),默认是 array.length
  1. 示例
  • 3.1 填充整个数组

    接着上述例子,如果想创建一个有默认值的数组:

const arr = new Array(5).fill(undefined); // 5个undefined
console.log(arr);
arr[8] = undefined;
console.log(arr);

8877b58ce8199fb9e6a0590b81da9319.png 这时候就可以用 for...inforEachmap 等方法处理。

  • 3.2 指定开始位置填充
const arr = [1, 2, 3, 4, 5];
arr.fill(0, 2); // 从索引 2 开始填充
console.log(arr); // [1, 2, 0, 0, 0]

在这个例子中,我们从索引 2 开始填充 0,因此索引 2 及其之后的所有元素都被替换成了 0

  • 3.3 指定开始和结束位置填充
const arr = [1, 2, 3, 4, 5];
arr.fill(0, 1, 4); // 从索引 1 开始填充,直到索引 4(不包含)
console.log(arr); // [1, 0, 0, 0, 5]
  • 3.4 使用对象作为填充值

    值得注意的是,如果使用对象作为填充值,所有填充的位置将引用同一个对象实例:

const arr = new Array(3).fill({}); // 注意:所有的元素都是对同一个空对象的引用
arr[0].key = "value";
console.log(arr); // [{ key: 'value' }, { key: 'value' }, { key: 'value' }]

在这个例子中,因为 {} 是一个对象,所有填充的位置都指向了同一个对象实例

因此修改其中一个对象会影响所有位置的对象

静态方法

来看例子:

console.log(Array.of(1,2,3))
console.log(Array.from(new Array(26),(val, index) => String.fromCodePoint(65 + index)))

image.png

Array.of()

需要根据一组值创建数组,并且不希望被单个数值参数特殊处理(即被视为数组长度)时,Array.of 是非常有用的。

  • 作用:创建并返回一个新的数组实例,包含所有作为参数传递给它的元素。

  • 特点

    • 不论传入多少参数,都会将这些参数作为新数组的实际元素。
    • 即使传入的是单个数字,也会将其视为数组中的一个元素,而不是数组的长度。

new Array()对比:

console.log(Array.of(7));       // [7]
console.log(Array.of(1, 2, 3)); // [1, 2, 3]

// 对比 new Array 的行为
console.log(Array(7));          // [ <7 empty items> ]
console.log(Array(1, 2, 3));    // [1, 2, 3]

new Array(7)这里的单个数字:7 被当做长度;

Array.of(7)这里的 7 被看做数组的元素。

Array.from()

Array.from() 方法用于将类数组(array-like)或可迭代对象(iterable)转换为真正的数组。

例子解析:

首先第一个参数new Array(26),表示一个长度为 26 的类数组空槽);第二个参数是一个映射函数,用于对每个元素进行转换后再放入新数组中。这里的作用是将每个“空槽” 映射成一个字符index代表索引,val代表当前的值,使用String.fromCodePoint(65 + index),将每个值转为字符。

为什么 new Array(26) 可以工作?

虽然 new Array(26) 创建的是“空槽”,但 Array.from() 在处理时会忽略空槽,而是根据数组的 .length 属性,从 0 到 length - 1 依次调用映射函数。

所以即使数组是空的,只要它有 .lengthArray.from() 也能正常工作。

for (let key in arr2) { // 迭代器
    console.log(key, arr2[key]); 
}
for (let item of arr2) { // 迭代器
    console.log(item); // 
}

image.png

obj.hasOwnProperty(prop)

这是 JavaScript 中用于判断一个对象是否拥有某个自身属性不是继承来的)的方法。

hasOwnProperty() 只检查对象自身的属性,不包括原型链.

  • obj:一个对象或数组;
  • prop:要检查的属性名(字符串或 Symbol);
  • 返回值:true 或 false

例子详解

const arr = new Array(5)
console.log(arr[0]); // undefined 

为什么 `arr[0]` 是 `undefined`,它不是空的吗?

🧩 答案:它不是真正的 undefined,而是“空槽(empty)”

因为前面讲到 new Array(5) 创建的是一个长为 5 的数组,但所有位置都为 空槽,当访问arr[0] 时,JS会返回undefined但这只是没有值的表现

实际上这个位置还没有被赋值,也不是真正的 undefined

可以理解为:

它像是一个“薛定谔的值”——你不去看它的时候,它是“空槽”;你一访问它,它就“坍缩”成了 undefined

如何区分“空槽”和“真的 undefined”?
console.log(arr.hasOwnProperty(0)); // false

✅ 这就是关键!

数组本质上是对象,索引是属性名(字符串形式)。

hasOwnProperty(0)返回 false,说明索引 0 并不是一个真实存在的属性

✅一句话总结

hasOwnProperty() 是 JavaScript 中判断一个对象或数组是否拥有某个“自身属性”的关键方法。它不会检查原型链,非常适合用来判断某个索引或 key 是否真实存在。结合 new Array(n) 使用,可以判断某个索引是否是“空槽”。


遍历数组

1. map: “一一对应”地加工数组

通俗理解:

你有一筐苹果,你想把它们都削成苹果片。map 就是那个削苹果的刀。

作用:

  • 对数组中的每个元素都做相同的操作
  • 返回一个新的数组,结构不变,但内容变了。

示例:

const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);
console.log(doubled); // [2, 4, 6]

把数组里的每个数都乘以 2,得到一个新数组。

2. filter: “挑挑拣拣”地筛选数组

通俗理解:

你有一堆水果,你想只留下苹果,filter 就是那个帮你挑拣的工具。

作用:

  • 按照你设定的条件,留下你想要的元素;
  • 返回一个新数组,只包含符合条件的元素。

示例:

const fruits = ['apple', 'banana', 'grape', 'apricot'];
const apples = fruits.filter(fruit => fruit.startsWith('a'));
console.log(apples); // ['apple', 'apricot']

只保留以字母 "a" 开头的水果。

3. reduce: “从头累加”地合并数组

通俗理解:

你有一堆钱,你想知道总共有多少钱,reduce 就是那个帮你“数钱”的人。

作用:

  • 把数组里的所有元素“合并成一个结果”;
  • 可以是数字、字符串、对象等;
  • 常用于求和、统计、分组等操作。

示例:

const nums = [1, 2, 3, 4];
const total = nums.reduce((sum, num) => sum + num, 0);
console.log(total); // 10

把数组里的数字一个个加起来,最后得到总和。

4. entries: “编号+内容”地查看数组

通俗理解:

你有一排抽屉,你想知道每个抽屉是第几个,里面装了什么。entries 就是那个帮你查编号和内容的工具。

作用:

  • 返回一个迭代器对象,可以用来遍历数组;
  • 每次遍历得到的是一个 [索引, 元素] 的数组;
  • 常配合 for...of 使用。
const arr = [1, 2, 3, 4, 5];
console.log(arr.entries());

image.png

示例:

const fruits = ['apple', 'banana', 'grape'];
const iterator = fruits.entries();

console.log(iterator.next().value); // [0, 'apple']
console.log(iterator.next().value); // [1, 'banana']
console.log(iterator.next().value); // [2, 'grape']

或者更常用写法:

for (let [index, value] of fruits.entries()) {
    console.log(index, value);
}
// 输出:
// 0 apple
// 1 banana
// 2 grape

一句话总结:

方法名通俗理解返回值
map一一加工新数组
filter挑拣符合条件的元素新数组
reduce从头到尾合并成一个值一个结果
entries查看编号+内容迭代器对象

for of 中的arr.entries()

for...of 的优势

const arr = [1, 2, 3, 4, 5];

for (let item of arr) {
    console.log(item);
}
  •  优点:代码简洁、语义清晰;
  • ❌ 缺点:只能拿到元素(item)拿不到索引(index)

如果我需要索引呢?

那就需要一个能同时返回索引和元素的东西。

这就引出了我们今天的主角: arr.entries()

const arr = [1, 2, 3, 4, 5];

for (const [index, item] of arr.entries()) {
    console.log(index, item);
}
//0 1
//1 2
//2 3
//3 4
//4 5

逻辑解释

  1. arr.entries() 返回一个迭代器对象;
  2. 每次 for...of 迭代时,会得到一个 [index, item]
  3. 使用 数组解构赋值[index, item] 直接把索引和值取出来;
  4. 然后你就可以同时操作 index 和 item 了。

小贴士

  • arr.entries() 返回的是一个迭代器对象,不能直接 console.log() 看到内容,只能看到返回一个迭代器对象。

image.png

  • 要配合 for...of 或 Array.from() 才能看到结果;

    • 与 Array.from()配合

    假设我们有一个数组,并且想要将其索引和值都转化为一个新数组:

const arr = ['a', 'b', 'c'];

// 使用 Array.from() 将 entries 迭代器转换为数组
const entriesArray = Array.from(arr.entries());

console.log(entriesArray);
// 输出: [[0, 'a'], [1, 'b'], [2, 'c']]

这里,arr.entries() 返回的是一个迭代器对象,而 Array.from() 将这个迭代器转换为了一个实际的数组

  • 如果你只想在 map 中使用,可以先用 Array.from(arr.entries()) 转成实际数组。

结尾

通过这篇文章,我们不仅学习了如何创建和填充数组,还掌握了数组遍历和操作的核心方法,如 mapfilterreduceentries,并通过 for...ofArray.from() 的配合,实现了更灵活的数组处理方式。

更重要的是,我们理解了 JavaScript 数组中一些“看似奇怪”的行为背后的原因,比如:

  • new Array(5) 创建的是“空槽”,而不是 undefined
  • hasOwnProperty() 可以帮助我们判断某个索引是否真实存在;
  • for...in 不适合遍历稀疏数组;
  • Array.from() 可以将“类数组”或“空槽数组”转换为可操作的真正数组。

掌握了这些知识,你将能够更自信地处理各种数组场景,写出更健壮、更高效的代码。