从0到1深入理解JavaScript数组:从基础到高级应用

100 阅读8分钟

前言

在JavaScript的世界里,数组(Array)无疑是最常用也是最重要的数据结构之一。无论是存储一系列数据,还是进行复杂的数据处理,数组都扮演着核心角色。作为一名稀土掘金的博主,今天我将带大家深入探索JavaScript数组的奥秘,从它的创建方式到各种强大的操作方法,让你彻底掌握数组的精髓。

1. 数组的创建:不止是 [ ] 那么简单

在JavaScript中,创建数组的方式多种多样,最常见的就是使用数组字面量[],但除此之外,new Array()Array.of()Array.from()也各有其独特的用途。

1.1 数组字面量 [ ]:简洁与直观

这是我们最常用也最推荐的创建数组的方式。它简洁、直观,并且能够直接初始化数组的元素。

const names = ['Alice', 'Bob', 'Candy', 'Sunny'];
console.log(names); // ['Alice', 'Bob', 'Candy', 'Sunny']

1.2 new Array():小心“坑”

使用new Array()构造函数创建数组时,需要特别注意它的行为。当传入一个数字参数时,它会创建一个指定长度的空数组,但这些位置是“空的”(empty),而不是 undefined

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

// 遍历这样的数组时,需要特别注意
for (let key in arr) {
  console.log(key, arr[key]); // 不会输出任何内容,因为没有“实际”的属性
}

// 如果想要填充undefined,可以使用fill方法
const arr2 = new Array(5).fill(undefined);
console.log(arr2); // [undefined, undefined, undefined, undefined, undefined]
for (let item of arr2) {
  console.log(item); // 会输出5个undefined
}

这种“空”的特性在某些遍历方法(如for...in)中表现得尤为明显,它不会遍历这些“空”的索引。而for...offorEach 等方法则会跳过这些空位,或者在访问时返回 undefined

1.3 Array.of():创建包含指定元素的数组

Array.of() 方法用于创建包含指定参数的新 Array 实例。与new Array()不同,Array.of()在处理单个数字参数时不会创建空数组,而是将其作为数组的唯一元素。

console.log(Array.of(1, 2, 3)); // [1, 2, 3]
console.log(Array.of(5));     // [5]  与 new Array(5) 的行为不同

1.4 Array.from():从类数组或可迭代对象创建数组

Array.from() 方法是一个非常强大的静态方法,它允许你从类数组对象(拥有一个 length 属性和可索引元素)或可迭代对象(如 MapSetString 等)创建一个新的、浅拷贝的Array实例。它还可以接受一个映射函数作为第二个参数,用于对每个元素进行处理。

// 从字符串创建数组
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']

// 从类数组对象创建数组
const obj = { 0: 'a', 1: 'b', length: 2 };
console.log(Array.from(obj)); // ['a', 'b']

// 结合映射函数,实现复杂转换
console.log(Array.from(new Array(26),
  (val, index) => String.fromCodePoint(index + 65)));
// ['A', 'B', 'C', ..., 'Z']

Array.from() 在处理需要转换或填充的场景时非常有用,例如生成连续的数字序列或字符序列。

2. 数组的特性:灵活与强大

JavaScript数组的灵活性是其强大之处。它不像C++或Java等语言中的数组那样,需要预先限定长度和类型。JavaScript数组可以动态扩容,并且可以存储不同类型的数据。

2.1 动态扩容与不限类型

const arr = [1, 2, 3];
arr[5] = 'hello'; // 动态扩容,arr变为 [1, 2, 3, empty, empty, 'hello']
arr.push(true); // 继续扩容
console.log(arr); // [1, 2, 3, empty, empty, 'hello', true]

这种特性使得JavaScript数组在处理不确定数量或类型的数据时非常方便。

2.2 hasOwnProperty与in运算符:属性检查的艺术

在JavaScript中,检查对象属性是否存在时,hasOwnProperty 方法和 in 运算符有着重要的区别。hasOwnProperty 方法只检查对象自身的属性,而 in 运算符会检查对象及其原型链上的属性。

const arr = new Array(5);
console.log(arr.hasOwnProperty(0)); // false (因为索引0是“空”的,不是实际的属性)

const obj1 = {
  name: 'Alice',
  age: 18
};
const obj2 = {
  skill: 'js'
};
obj1.__proto__ = obj2; // 设置原型链

console.log(obj1.skill); // js (通过原型链访问)

// in 运算符会检查原型链上的属性
for (const key in obj1) {
  console.log(key, obj1[key]);
  // 输出: name Alice, age 18, skill js
}

// hasOwnProperty 方法只对对象的直接属性返回true,对于继承的属性则返回false
console.log(obj1.hasOwnProperty('name'));  // true
console.log(obj1.hasOwnProperty('skill')); // false

理解这两者的区别对于编写健壮的代码至关重要,尤其是在遍历对象属性时。

3. 数组的遍历:多种姿势,灵活选择

遍历数组是日常开发中最常见的操作之一。JavaScript提供了多种遍历数组的方法,每种方法都有其适用场景和特点。

3.1 传统for循环:性能与控制

传统的for循环提供了最细粒度的控制,适用于需要精确控制循环过程(如跳出循环、修改索引)的场景。但其可读性相对较差。

const names = ['Alice', 'Bob', 'David', 'Eva'];
for (let i = 0; i < names.length; i++) {
  console.log(names[i]);
}

3.2 forEach:简单遍历,无返回值

forEach 方法用于遍历数组的每个元素,并对每个元素执行回调函数。它没有返回值,主要用于执行副作用操作。

const names = ['Alice', 'Bob', 'David', 'Eva'];
names.forEach(name => {
  if (name === 'Eva') {
    console.log('I am ' + name);
    return; // 注意:forEach中的return只会跳过当前迭代,不会终止整个循环
  }
  console.log('Processing ' + name);
});

3.3 map:转换数组,生成新数组

map 方法用于创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。它不会改变原数组。

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // [2, 4, 6]

3.4 filter:筛选数组,生成新数组

filter 方法创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。它也不会改变原数组。

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]

3.5 find:查找第一个匹配元素

find 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const alice = users.find(user => user.name === 'Alice');
console.log(alice); // { id: 1, name: 'Alice' }

3.6 some:检查是否有元素满足条件

some 方法测试数组中是不是至少有一个元素通过了被提供的函数检测。它返回一个布尔值。

const numbers = [1, 2, 3, 4, 5];
const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true

3.7 every:检查所有元素是否满足条件

every 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

const numbers = [2, 4, 6];
const allEven = numbers.every(num => num % 2 === 0);
console.log(allEven); // true

3.8 for...of:遍历可迭代对象的元素值

for...of 循环用于遍历可迭代对象(包括 ArrayStringMapSet等)的元素值。它比传统的for循环更具可读性,并且能够直接获取元素的值。

const arr = [1, 2, 3];
for (let item of arr) {
  console.log(item); // 1, 2, 3
}

// 获取索引和值
for (const [index, value] of arr.entries()) {
  console.log(index, value); // 0 1, 1 2, 2 3
}

3.9 for...in:遍历对象的可枚举属性名

for...in 循环主要用于遍历对象的可枚举属性名(包括原型链上的属性)。不推荐用于遍历数组,因为它会遍历到非数字属性,并且遍历顺序不确定。

const arr = [1, 2, 3];
arr.myProperty = 'test';
for (let key in arr) {
  console.log(key, arr[key]); // 0 1, 1 2, 2 3, myProperty test
}

4. 数组的常用方法:高效处理数据

除了遍历方法,JavaScript数组还提供了许多其他实用的方法,帮助我们高效地处理数据。

4.1 fill():填充数组元素

fill()方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

const arr = [1, 2, 3, 4, 5];
arr.fill(0, 2, 4); // 从索引2开始,到索引4(不包含)填充0
console.log(arr); // [1, 2, 0, 0, 5]

4.2 reduce():数组归约,生成单一结果

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。它非常适合进行累加、累乘、对象转换等操作。

console.log([1, 2, 3, 4, 5, 6].reduce((prev, curr) => {
  return prev + curr;
}, 0)); // 21 (0是初始值)

// 统计字符出现次数
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const wordCount = words.reduce((acc, word) => {
  acc[word] = (acc[word] || 0) + 1;
  return acc;
}, {});
console.log(wordCount); // { apple: 3, banana: 2, orange: 1 }

reduce() 的强大之处在于它能够将数组“消灭”,最终得到一个“唯一的对的状态”,这个新的状态是基于上一个状态计算得出的。

总结

JavaScript数组作为一种灵活且强大的数据结构,在前端开发中扮演着举足轻重的角色。从简单的字面量创建到复杂的Array.from(),从传统的for循环到现代的for...of和各种高阶函数,掌握这些知识能够让你在处理数据时游刃有余。希望通过本文的详细介绍,你对JavaScript数组有了更深入的理解,并能在实际项目中灵活运用!