{T}

数组基础

数组是 JavaScript 中最常用、最灵活的数据结构之一。本章将全面介绍数组的基础概念、创建方式、核心特性和基本操作。

本章概览

本章是 JavaScript 数组系列的开篇,主要内容包括:

code
数组基础
├── 核心特性 - 动态大小、类型灵活、稀疏数组等
├── 创建方式 - 字面量、构造函数、ES6+新方法
├── 索引与访问 - 索引操作、length属性、at()方法
├── 数组检测 - isArray()方法及多种检测方案
├── 遍历方法 - for循环、for...in、for...of等
├── 类数组对象 - arguments、NodeList等
└── 进阶特性 - 稀疏数组、解构赋值、扩展运算符

相关章节导航

数组核心特性

ECMAScript 中的数组是一种特殊的对象,每一项可以保存任何类型的数据。数组的大小可以动态调整,即可以随着数据的添加自动增长以容纳新增数据。

// ... 中间省略 ...

  • 推荐:转换类数组对象时使用 Array.from() 或扩展运算符
  • 避免:使用 new Array() 创建单元素数组,容易产生歧义

构造函数

数组的创建方式有两种:数组字面量构造函数。使用构造函数创建数组的形式:

code
let array = new Array()

已知数组元素数量,那么可以给构造函数传入一个数值,然后 length 属性就会被自动创建并保存这个值。比如创建长度为 10 的数组,数组每个元素的值都是 undefined。还可以给 Array 构造函数传入要保存的元素,比如:

javascript
let array = new Array(10)

let colors = new Array("red", "blue", "green")

⚠️ 给构造函数传递一个值也可以创建数组。注意: - 如果传递的是数值,则会按照该数值创建包含给定项数的数组

  • 如果传递的是其他类型的参数,则会创建包含那个值的只有一项的数组
javascript
let colors = new Array(3) // 创建一个包含 3 个元素的数组
let names = new Array("Greg") // 创建一个只包含一个元素,即字符串"Greg"的数组

在使用 Array 构造函数时可以省略 new 操作符:

javascript
let colors = Array(3) // 创建一个包含 3 个元素的数组
let names = Array("Greg") // 创建一个只包含一个元素,即字符串"Greg"的数组

数组字面量

创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开,如下所示:

javascript
var colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
var names = [] // 创建一个空数组
var values = [1, 2] // 创建一个包含 2 个元素的数组
var options = [, , , , ,] // 不推荐!创建包含空位的稀疏数组

鉴于数组的常用性,ES6 专门扩展数组构造器 Array ,新增 2 个方法:Array.ofArray.fromArray.of 用得比较少,Array.from 具有很强的灵活性

Array.of

Array.of 可以把一组参数转换为数组。这个方法用于替代在 ES6 之前常用的 Array.prototype.slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:

javascript
console.log(Array.of(1, 2, 3, 4)) // [1, 2, 3, 4]
console.log(Array.of(undefined)) // [undefined]

在下面的代码中,可以看到:当参数为 2 个时,返回的结果是一致的;当参数是一个时,Array.of 会把参数变成数组里的一项,而构造器则会生成长度和第一个参数相同的空数组

javascript
Array.of(8.0) // [8]
Array(8.0) // [empty × 8]

Array.of(8.0, 5) // [8, 5]
Array(8.0, 5) // [8, 5]

Array.of("8") // ["8"]
Array("8") // ["8"]

Array.from

Array.from 有 3 个参数

  • 类似数组的对象,必选
  • 加工函数,新生成的数组会经过该函数的加工再返回,可选
  • this 作用域,表示加工函数执行时 this 的值,可选
javascript
// 字符串会被拆分为单字符数组
console.log(Array.from("Matt")) // ["M", "a", "t", "t"]

// 可以使用 from() 将集合和映射转换为一个新数组
const m = new Map().set(1, 2).set(3, 4)
const s = new Set().add(1).add(2).add(3).add(4)
console.log(Array.from(m)) // [[1, 2], [3, 4]]
console.log(Array.from(s)) // [1, 2, 3, 4]

Array.from 可以对现有数组执行浅复制

javascript
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1)
console.log(a1) // [1, 2, 3, 4]
alert(a1 === a2) // false
javascript
// 可以使用任何可迭代对象
const iter = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
    yield 4
  }
}
console.log(Array.from(iter)) // [1, 2, 3, 4]

// arguments 对象可以被轻松地转换为数组
function getArgsArray() {
  return Array.from(arguments)
}
console.log(getArgsArray(1, 2, 3, 4)) // [1, 2, 3, 4]

// from() 也能转换带有必要属性的自定义对象
const arrayLikeObject = {
  0: 1,
  1: 2,
  2: 3,
  3: 4,
  length: 4
}
console.log(Array.from(arrayLikeObject)) // [1, 2, 3, 4]

Array.from 还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像调用 Array.from().map() 那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函数中 this 的值。但这个重写的 this 值在箭头函数中不适用

javascript
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1, (x) => x ** 2)
const a3 = Array.from(
  a1,
  function (x) {
    return x ** this.exponent
  },
  { exponent: 2 }
)

console.log(a2) // [1, 4, 9, 16]
console.log(a3) // [1, 4, 9, 16]

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6 规范重新定义了该如何处理这些空位

javascript
const options = [, , , , ,] // 创建包含 5 个元素的数组
console.log(options.length) // 5
console.log(options) // [,,,,,]

ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined

javascript
const options = [1, , , , 5]

for (const option of options) {
  console.log(option === undefined)
}
// false、true、true、true、false

使用 ES6Array.from 创建数组

javascript
const a = Array.from([, , ,]) // 使用 ES6 的 Array.from()创建的包含 3 个空位的数组
for (const val of a) {
  alert(val === undefined)
}
// true、true、true
alert(Array.of(...[, , ,])) // [undefined, undefined, undefined]

for (const [index, value] of options.entries()) {
  alert(value)
}
// 1、undefined、undefined、undefined、5

而 ES6 之前的方法则会忽略这个空位

javascript
const options = [1, , , , 5]

// map()会跳过空位置
console.log(options.map(() => 6)) // [6, undefined, undefined, undefined, 6]

// join()视空位置为空字符串
console.log(options.join("-")) // "1----5"

数组索引和访问

在数组中可以通过使用数组的索引来获取和设置数组的值。数组索引从 0 开始,最大索引为 length - 1

javascript
let colors = ["red", "blue", "green"] // 定义一个字符串数组
alert(colors[0]) // 显示第一项
colors[2] = "black" // 修改第三项
colors[3] = "brown" // 添加第四项

如果把一个值设置给超过数组最大索引的索引,就像示例中的 colors[3],则数组长度会自动扩展到该索引值加 1(示例中设置的索引 3,所以数组长度变成了 4)

javascript
let colors = new Array("red", "blue", "green")
colors.length = 2
console.log(colors[2]) // undefined

colors.length = 4
console.log(colors[3]) // undefined
javascript
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
colors.length = 2
alert(colors[2]) // undefined

如果将 length 设置为大于数组元素数的值,则新添加的元素都将以 undefined 填充:

javascript
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组

colors.length = 4
alert(colors[3]) // undefined

使用 length 属性可以方便地向数组末尾添加元素,如下例所示:

javascript
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组

colors[colors.length] = "black" //添加一种颜色(位置3)
colors[colors.length] = "brown" //再添加一种颜色(位置4)

colors 数组有一个值被插入到位置 99,结果新 length 就变成了 100。这中间的所有元素,即位置 3~98,实际上并不存在,因此在访问时会返回 undefined

javascript
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组

colors[99] = "black" //添加一种颜色(位置99)
alert(colors.length) // 100

使用 at() 方法访问元素(ES2022)

ES2022 引入了 at() 方法,支持使用负数索引从数组末尾访问元素:

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

// 正数索引(从前往后)
console.log(arr.at(0)) // 1
console.log(arr.at(2)) // 3

// 负数索引(从后往前)
console.log(arr.at(-1)) // 5(最后一个元素)
console.log(arr.at(-2)) // 4(倒数第二个元素)

// 超出范围返回 undefined
console.log(arr.at(10)) // undefined
console.log(arr.at(-10)) // undefined

at() 方法的优势:

  • 支持负数索引,访问末尾元素更方便
  • arr[arr.length - 1] 更简洁
  • 与字符串的 at() 方法行为一致

检测数组 isArray

经典的 ECMAScript 问题是判断一个对象是不是数组。在只有一个网页(因而只有一个全局作用域)的情况下,使用 instanceof 操作符就够了:

javascript
if (value instanceof Array) {
  // 操作数组
}

使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。

为解决这个问题,ECMAScript 提供 Array.isArray() 方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的

javascript
if (Array.isArray(value)) {
  // 操作数组
}

在 ES6 之前,至少有如下 5 种方式去判断一个对象是否为数组

javascript
Object.prototype.toString.call(obj).slice(8, -1) === "Array"

obj.constructor === Array

obj instanceof Array

Array.prototype.isPrototypeOf(obj)

Object.getPrototypeOf(obj) === Array.prototype

如果 obj 是一个数组,那么上面这 5 个判断全部为 true

性能对比

javascript
const arr = [1, 2, 3]

// 性能测试(仅供参考,实际结果可能因环境而异)
// Array.isArray() - 最快,推荐使用
Array.isArray(arr) // true

// Object.prototype.toString - 兼容性好
Object.prototype.toString.call(arr) === "[object Array]" // true

// instanceof - 在多框架环境下可能有问题
arr instanceof Array // true

// constructor - 可能被修改
arr.constructor === Array // true

数组遍历方法

JavaScript 提供了多种遍历数组的方法,每种方法都有其适用场景。

for…in

for…in 主要用于对数组或者对象的属性进行循环操作。循环中的代码每执行一次,就会对对象的属性进行一次操作

javascript
const arr = [1, 2, 3]

for (var i in arr) {
  console.log("键名:", i)
  console.log("键值:", arr[i])
}

// 键名: 0
// 键值: 1
// 键名: 1
// 键值: 2
// 键名: 2
// 键值: 3

for...of

for...of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of 循环,以替代 for...inforEach,并支持新的迭代协议。for...of 允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。

该方法允许获取对象的键值:

javascript
var arr = ["a", "b", "c", "d"]
for (let a in arr) {
  console.log(a) // 0 1 2 3
}

for (let a of arr) {
  console.log(a) // a b c d
}

该方法只会遍历当前对象的属性,不会遍历其原型链上的属性。

注意:

  • for...of 适用遍历 数组/ 类数组/字符串/map/set 等拥有迭代器对象的集合
  • 它可以正确响应 breakcontinuereturn 语句;
  • for...of 循环不支持遍历普通对象,因为没有迭代器对象。如果想要遍历一个对象的属性,可以用 for-in 循环

总结,for…of 和 for…in 的区别如下:

特性for...infor...of
遍历内容键名(索引)键值(元素)
原型链会遍历原型链上的可枚举属性只遍历当前对象
性能较慢(需要检查原型链)较快
适用场景对象属性遍历数组、字符串、Map、Set 等可迭代对象
推荐度不推荐用于数组推荐用于数组

传统 for 循环

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

// 标准 for 循环
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i])
}

// 优化:缓存 length
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i])
}

// 倒序遍历
for (let i = arr.length - 1; i >= 0; i--) {
  console.log(arr[i])
}

forEach 方法

javascript
const arr = [1, 2, 3]

// forEach 遍历(注意:无法使用 break/continue)
arr.forEach((item, index, array) => {
  console.log(`索引 ${index}: 值 ${item}`)
})

// 注意:forEach 无法中断循环
arr.forEach((item) => {
  if (item === 2) {
    return // 只能跳过当前迭代,不能中断整个循环
  }
  console.log(item)
})

使用 entries()、keys()、values() 遍历

ES6 提供了三个迭代器方法,可以获取数组的索引、值或键值对:

javascript
const arr = ["a", "b", "c"]

// keys() - 获取索引
for (const index of arr.keys()) {
  console.log(index) // 0, 1, 2
}

// values() - 获取值(与 for...of 等价)
for (const value of arr.values()) {
  console.log(value) // 'a', 'b', 'c'
}

// entries() - 获取 [索引, 值] 对
for (const [index, value] of arr.entries()) {
  console.log(index, value) // 0 'a', 1 'b', 2 'c'
}

// 转换为数组
console.log(Array.from(arr.keys())) // [0, 1, 2]
console.log(Array.from(arr.values())) // ['a', 'b', 'c']
console.log(Array.from(arr.entries())) // [[0, 'a'], [1, 'b'], [2, 'c']]

类数组对象

JavaScript 中,主要有以下情况中的对象是类数组

  • 函数里面的参数对象 arguments
  • getElementsByTagName/ClassName/Name 获得的 HTMLCollection
  • querySelector 获得的 NodeList

在日常开发中经常会遇到各种类数组对象,最常见的就是在函数中使用的 arguments,它的对象只定义在函数体中,包括了函数的参数和其他属性:

javascript
function foo(name, age, sex) {
  console.log(arguments)
  console.log(typeof arguments) // object
  console.log(Object.prototype.toString.call(arguments)) //  [object arguments]
}
foo("jack", "18", "male")

length 属性就是函数参数的长度。另外 arguments 还有一个 callee 属性,指向当前正在执行的函数。

javascript
function foo(name, age, sex) {
  console.log(arguments.callee)
}

foo("jack", "18", "male")

打印结果如下:

javascript
ƒ foo(name, age, sex) {
  console.log(arguments.callee);
}

HTMLCollection 简单来说是 HTML DOM 对象的一个接口,这个接口包含获取到的 DOM 元素集合,返回的类型是类数组对象,如果用 typeof 来判断的话,它返回的是 object。它是及时更新的,当文档中的 DOM 变化时,它也会随之变化

javascript
var elem1, elem2
// document.forms 是一个 HTMLCollection
elem1 = document.forms[0]
elem2 = document.forms.item(0)
console.log(elem1)
console.log(elem2)
console.log(typeof elem1)
console.log(Object.prototype.toString.call(elem1))

NodeList 对象是节点的集合,通常是由 querySlector 返回的。NodeList 也是一种类数组。虽然 NodeList 不是一个数组,但是可以使用 for...of 来迭代。在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化

javascript
var list = document.querySelectorAll("input[type=checkbox]")
for (var checkbox of list) {
  checkbox.checked = true
}
console.log(list)
console.log(typeof list)
console.log(Object.prototype.toString.call(list))

应用场景

在函数内部可以直接获取 arguments 这个类数组的值,那么也可以对于参数进行一些操作

javascript
function add() {
  var sum =0,len = arguments.length;
  for(var i = 0; i < len; i++){
    sum += arguments[i];
  }
  return sum;
}

add();                         // 0
add(1);                          // 1
add(1,2);                       // 3
add(1,2,3,4);                    // 10

可以通过 arguments 这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串,该参数指定一个字符作为衔接点来连接字符串

javascript
function myConcat(separa) {
  var args = Array.prototype.slice.call(arguments, 1)
  return args.join(separa)
}

myConcat(", ", "red", "orange", "blue")
// "red, orange, blue"

myConcat("; ", "elephant", "lion", "snake")
// "elephant; lion; snake"

myConcat(". ", "one", "two", "three", "four", "five")
// "one. two. three. four. five"

这段代码说明可以传递任意数量的参数到该函数,并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出,可以在日常编码中采用这样的代码抽象方式,把需要解决的这一类问题,都抽象成通用的方法,来提升代码的可复用性

可以借助 applycallarguments 相结合,将参数从一个函数传递到另一个函数

javascript
// 使用 apply 将 foo 的参数传递给 bar
function foo() {
  bar.apply(this, arguments)
}

function bar(a, b, c) {
  console.log(a, b, c)
}

foo(1, 2, 3) //1 2 3

转为数组

类数组因为不是真正的数组,所以没有数组类型上自带的那些方法,所以就需要利用下面这几个方法去借用数组的方法。比如借用数组的 push 方法

javascript
var arrayLike = {
  0: "java",
  1: "script",
  length: 2
}

Array.prototype.push.call(arrayLike, "jack", "lily")
console.log(typeof arrayLike) // 'object'
console.log(arrayLike)
// {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}

这里用 call 的方法来借用 Array 原型链上的 push 方法,可以实现一个类数组的 push 方法,给 arrayLike 添加新的元素。

arguments 如何转换成数组:

javascript
function sum(a, b) {
  let args = Array.prototype.slice.call(arguments)
  // let args = [].slice.call(arguments); // 这样写也是一样效果
  console.log(args.reduce((sum, cur) => sum + cur))
}

sum(1, 2) // 3

function sum(a, b) {
  let args = Array.prototype.concat.apply([], arguments)
  console.log(args.reduce((sum, cur) => sum + cur))
}
sum(1, 2) // 3

还可以采用 ES6 新增的 Array.from 方法 或 展开运算符的方法来将类数组转化为数组

javascript
function sum(a, b) {
  let args = Array.from(arguments)
  console.log(args.reduce((sum, cur) => sum + cur))
}
sum(1, 2) // 3

function sum(a, b) {
  let args = [...arguments]
  console.log(args.reduce((sum, cur) => sum + cur))
}
sum(1, 2) // 3

function sum(...args) {
  console.log(args.reduce((sum, cur) => sum + cur))
}
sum(1, 2) // 3

💡 在现代 JavaScript 开发中,推荐使用剩余参数**(...args)替代 arguments 对象,因为:** - 剩余参数是真正的数组,可以直接使用数组方法

  • 代码更简洁、更易读
  • 支持箭头函数(箭头函数没有 arguments 对象)

数组的稀疏性

JavaScript 数组可以是密集数组(dense array)或稀疏数组(sparse array)。

密集数组

密集数组是指所有索引都有值的数组:

javascript
const dense = [1, 2, 3, 4, 5]
console.log(dense.length) // 5

稀疏数组

稀疏数组是指某些索引位置没有元素的数组:

javascript
// 创建稀疏数组的方式
const sparse1 = [1, , , 4] // 字面量中省略元素
const sparse2 = new Array(5) // 使用构造函数
const sparse3 = []
sparse3[0] = 1
sparse3[5] = 6 // 跳过中间索引

console.log(sparse1.length) // 4
console.log(sparse1) // [1, empty × 2, 4]
console.log(1 in sparse1) // false(索引 1 不存在)
console.log(3 in sparse1) // true(索引 3 存在)

检查数组元素是否存在

javascript
const arr = [1, , 3]

// 使用 in 操作符
console.log(0 in arr) // true
console.log(1 in arr) // false(空位)
console.log(2 in arr) // true

// 使用 hasOwnProperty
console.log(arr.hasOwnProperty(0)) // true
console.log(arr.hasOwnProperty(1)) // false

// 直接访问(不推荐,无法区分 undefined 和空位)
console.log(arr[1]) // undefined
console.log(arr[1] === undefined) // true(但无法确定是空位还是值为 undefined)

性能考虑

稀疏数组在某些操作中可能表现不佳:

javascript
const sparse = new Array(1000000) // 创建稀疏数组
sparse[999999] = "last"

// 某些方法会跳过空位
sparse.forEach((item) => console.log(item)) // 只输出 'last'

// 某些方法会将空位视为 undefined
const mapped = sparse.map((x) => x) // 会创建大量 undefined 值

数组的转换方法

toString() 和 toLocaleString()

javascript
const arr = [1, 2, 3, null, undefined]

// toString() - 将数组转换为逗号分隔的字符串
console.log(arr.toString()) // "1,2,3,,"
console.log(String(arr)) // "1,2,3,,"(隐式调用 toString)

// toLocaleString() - 使用本地化格式
const dateArr = [new Date(), new Date()]
console.log(dateArr.toString()) // "Mon Jan 01 2024..., Mon Jan 01 2024..."
console.log(dateArr.toLocaleString()) // 本地化日期格式

join() 方法

join() 方法可以将数组元素连接成字符串,可以指定分隔符:

javascript
const arr = ["red", "green", "blue"]

console.log(arr.join()) // "red,green,blue"(默认逗号)
console.log(arr.join("")) // "redgreenblue"(无分隔符)
console.log(arr.join("-")) // "red-green-blue"
console.log(arr.join(" | ")) // "red | green | blue"

// 处理空值和 undefined
const arr2 = [1, null, undefined, 4]
console.log(arr2.join("-")) // "1--4"(null 和 undefined 转为空字符串)

数组解构

ES6 引入了数组解构(destructuring),可以方便地从数组中提取值:

javascript
// 基本解构
const arr = [1, 2, 3]
const [a, b, c] = arr
console.log(a, b, c) // 1, 2, 3

// 跳过某些元素
const [first, , third] = arr
console.log(first, third) // 1, 3

// 默认值
const [x, y, z = 10] = [1, 2]
console.log(x, y, z) // 1, 2, 10

  // ... 中间省略 ...


// 函数返回值解构
function getNumbers() {
  return [1, 2, 3]
}
const [x, y, z] = getNumbers()

扩展运算符

扩展运算符(spread operator)... 可以将数组展开为独立的元素:

javascript
// 展开数组
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const combined = [...arr1, ...arr2]
console.log(combined) // [1, 2, 3, 4, 5, 6]

// 复制数组(浅拷贝)
const original = [1, 2, 3]
const copy = [...original]
console.log(copy) // [1, 2, 3]
console.log(copy === original) // false


  // ... 中间省略 ...

const chars = [...str]
console.log(chars) // ['h', 'e', 'l', 'l', 'o']

// 类数组转数组
const nodeList = document.querySelectorAll("div")
const divArray = [...nodeList]

💡 扩展运算符和 Array.from() 的区别: - Array.from() 可以处理任何可迭代对象和类数组对象

  • 扩展运算符只能处理可迭代对象
  • 扩展运算符更简洁,但 Array.from() 功能更强大

数组的性能优化建议

  1. 预分配数组大小:如果知道数组大小,可以预先设置 length
  2. 避免稀疏数组:使用 undefined 代替空位
  3. 选择合适的遍历方法for 循环通常最快,forEach 次之
  4. 避免频繁修改 length:直接修改 length 可能触发性能问题
  5. 使用类型化数组:对于数值计算,考虑使用 TypedArray
javascript
// 性能优化示例
const arr = new Array(1000) // 预分配大小
arr.fill(0) // 填充初始值

// 使用 for 循环(通常最快)
for (let i = 0; i < arr.length; i++) {
  // 处理逻辑
}

// 避免在循环中修改数组长度
// 不好的做法
for (let i = 0; i < arr.length; i++) {
  if (condition) {
    arr.splice(i, 1) // 会改变数组长度,可能导致问题
    i-- // 需要手动调整索引
  }
}

// 更好的做法:从后往前遍历或使用 filter
arr.filter((item) => !condition)

常见问题解答 (FAQ)

Q1: 如何判断一个变量是否为数组?

javascript
// ✅ 推荐方式
Array.isArray(arr)

// ❌ 不推荐的方式
arr instanceof Array        // 在多框架环境下可能失效
arr.constructor === Array   // constructor 可能被修改

Q2: 数组和对象有什么区别?

特性数组对象
索引数字索引字符串键
顺序有序无序(ES2015+保持插入顺序)
length属性自动维护需手动维护
迭代for...of, 数组方法for...in, Object.keys()等
用途有序数据集合键值对存储
javascript
// 数组
const arr = ['a', 'b', 'c']
console.log(arr[0])  // 'a'
console.log(arr.length)  // 3

// 对象
const obj = {0: 'a', 1: 'b', 2: 'c'}
console.log(obj[0])  // 'a'
console.log(obj.length)  // undefined

Q3: 如何避免数组方法修改原数组?

javascript
// 创建数组副本
const original = [1, 2, 3]

// 方法1:扩展运算符
const copy1 = [...original]

// 方法2:Array.from()
const copy2 = Array.from(original)

// 方法3:slice()
const copy3 = original.slice()

// 方法4:concat()
const copy4 = original.concat()

// 注意:这些都是浅拷贝!
// 对于嵌套数组或对象,需要深拷贝
const deepCopy = JSON.parse(JSON.stringify(original))

Q4: 如何正确处理稀疏数组?

javascript
// ❌ 避免:创建稀疏数组
const sparse = [1, , , 4]

// ✅ 推荐:显式使用 undefined
const dense = [1, undefined, undefined, 4]

// 检查元素是否存在
console.log(1 in sparse)  // false(空位)
console.log(1 in dense)   // true(值为 undefined)

// 跳过空位
sparse.forEach(x => console.log(x))  // 1, 4(跳过空位)
dense.forEach(x => console.log(x))   // 1, undefined, undefined, 4

Q5: 如何选择合适的数组遍历方法?

快速选择指南:

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

// 1. 只需要遍历,不需要返回值
arr.forEach(x => console.log(x))

// 2. 需要返回新数组
const doubled = arr.map(x => x * 2)

// 3. 需要过滤元素
const evens = arr.filter(x => x % 2 === 0)

// 4. 需要累积结果
const sum = arr.reduce((acc, x) => acc + x, 0)

// 5. 需要查找元素
const found = arr.find(x => x > 3)
const index = arr.findIndex(x => x > 3)

// 6. 需要检查条件
const hasEven = arr.some(x => x % 2 === 0)
const allPositive = arr.every(x => x > 0)

// 7. 需要提前退出循环
for (const x of arr) {
  if (x === 3) break
  console.log(x)
}

性能对比:

方法相对性能可读性适用场景
for 循环⭐⭐⭐⭐⭐⭐⭐⭐性能敏感场景
for...of⭐⭐⭐⭐⭐⭐⭐⭐⭐需要提前退出
forEach⭐⭐⭐⭐⭐⭐⭐⭐简单遍历
map⭐⭐⭐⭐⭐⭐⭐⭐⭐数据转换
reduce⭐⭐⭐⭐⭐⭐复杂聚合

Q6: 类数组对象和数组的区别是什么?

javascript
// 类数组对象
const arrayLike = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
}

// 数组
const arr = ['a', 'b', 'c']

// 区别1:类型不同
console.log(Array.isArray(arrayLike))  // false
console.log(Array.isArray(arr))        // true

// 区别2:原型不同
console.log(arrayLike instanceof Array)  // false
console.log(arr instanceof Array)        // true

// 区别3:方法不同
// arrayLike.push('d')  // TypeError: arrayLike.push is not a function
arr.push('d')          // ✅ 正常工作

// 转换为真正的数组
const realArray = Array.from(arrayLike)
console.log(Array.isArray(realArray))  // true

最佳实践总结

1. 数组创建

推荐做法:

javascript
// 使用字面量
const arr = [1, 2, 3]

// 使用 Array.from() 转换类数组
const arrayLike = {0: 'a', 1: 'b', length: 2}
const arr = Array.from(arrayLike)

// 使用扩展运算符复制数组
const copy = [...arr]

避免做法:

javascript
// 构造函数的单参数歧义
const arr = new Array(3)  // 创建长度为3的空数组

// 创建稀疏数组
const sparse = [1, , , 4]

2. 数组遍历

推荐做法:

javascript
// 简单遍历
arr.forEach(item => console.log(item))

// 数据转换
const doubled = arr.map(x => x * 2)

// 需要退出循环时使用 for...of
for (const item of arr) {
  if (condition) break
  // 处理逻辑
}

避免做法:

javascript
// 在 forEach 中使用 return 或 break
arr.forEach(item => {
  if (condition) return  // ❌ 只能跳过当前迭代,不能退出循环
})

// 使用 for...in 遍历数组
for (const index in arr) {  // ❌ 会遍历原型链,不推荐用于数组
  console.log(arr[index])
}

3. 数组修改

推荐做法:

javascript
// 使用不可变操作(不修改原数组)
const newArr = [...arr, newItem]
const filtered = arr.filter(x => x > 0)
const mapped = arr.map(x => x * 2)

避免做法:

javascript
// 直接修改原数组(除非明确需要)
arr.push(newItem)
arr.splice(0, 1)

// 在遍历中修改数组长度
arr.forEach((item, index) => {
  if (condition) {
    arr.splice(index, 1)  // ❌ 可能导致意外行为
  }
})

4. 性能优化

推荐做法:

javascript
// 预分配数组大小
const arr = new Array(1000)
arr.fill(0)

// 使用 for 循环处理大数据
for (let i = 0, len = arr.length; i < len; i++) {
  // 处理逻辑
}

// 避免频繁的数组扩展
const result = []
for (let i = 0; i < 1000; i++) {
  result.push(i)  // 每次可能触发重新分配
}
// 更好:预先知道大小
const result = new Array(1000)
for (let i = 0; i < 1000; i++) {
  result[i] = i
}

5. 代码可读性

推荐做法:

javascript
// 使用语义化的方法名
const activeUsers = users.filter(user => user.isActive)
const userNames = users.map(user => user.name)
const hasAdmin = users.some(user => user.role === 'admin')

// 使用解构赋值
const [first, second, ...rest] = arr

// 使用命名良好的变量
const sum = numbers.reduce((total, num) => total + num, 0)

小结

本章全面介绍了 JavaScript 数组的基础知识:

  1. 核心特性:动态大小、类型灵活、对象本质
  2. 创建方式:字面量、构造函数、Array.of()、Array.from()
  3. 索引访问:数字索引、length属性、at()方法
  4. 数组检测:Array.isArray() 及其他方法
  5. 遍历方法:for循环、for...in、for...of、迭代器方法
  6. 类数组对象:arguments、NodeList及其转换
  7. 进阶特性:稀疏数组、解构赋值、扩展运算符

下一章预告:在数组方法中,我们将详细介绍数组的各种内置方法,包括增删改查、排序搜索、迭代处理等。


💡 学习建议:掌握数组基础是学习 JavaScript 的重要一步。建议多实践、多调试,理解数组方法的工作原理和性能特点。