数组基础
数组是 JavaScript 中最常用、最灵活的数据结构之一。本章将全面介绍数组的基础概念、创建方式、核心特性和基本操作。
本章概览
本章是 JavaScript 数组系列的开篇,主要内容包括:
数组基础
├── 核心特性 - 动态大小、类型灵活、稀疏数组等
├── 创建方式 - 字面量、构造函数、ES6+新方法
├── 索引与访问 - 索引操作、length属性、at()方法
├── 数组检测 - isArray()方法及多种检测方案
├── 遍历方法 - for循环、for...in、for...of等
├── 类数组对象 - arguments、NodeList等
└── 进阶特性 - 稀疏数组、解构赋值、扩展运算符相关章节导航
数组核心特性
ECMAScript 中的数组是一种特殊的对象,每一项可以保存任何类型的数据。数组的大小可以动态调整,即可以随着数据的添加自动增长以容纳新增数据。
// ... 中间省略 ...
- ✅ 推荐:转换类数组对象时使用
Array.from()或扩展运算符- ❌ 避免:使用
new Array()创建单元素数组,容易产生歧义
构造函数
数组的创建方式有两种:数组字面量和构造函数。使用构造函数创建数组的形式:
let array = new Array()已知数组元素数量,那么可以给构造函数传入一个数值,然后 length 属性就会被自动创建并保存这个值。比如创建长度为 10 的数组,数组每个元素的值都是 undefined。还可以给 Array 构造函数传入要保存的元素,比如:
let array = new Array(10)
let colors = new Array("red", "blue", "green")⚠️ 给构造函数传递一个值也可以创建数组。注意: - 如果传递的是数值,则会按照该数值创建包含给定项数的数组
- 如果传递的是其他类型的参数,则会创建包含那个值的只有一项的数组
let colors = new Array(3) // 创建一个包含 3 个元素的数组
let names = new Array("Greg") // 创建一个只包含一个元素,即字符串"Greg"的数组在使用 Array 构造函数时可以省略 new 操作符:
let colors = Array(3) // 创建一个包含 3 个元素的数组
let names = Array("Greg") // 创建一个只包含一个元素,即字符串"Greg"的数组数组字面量
创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开,如下所示:
var colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
var names = [] // 创建一个空数组
var values = [1, 2] // 创建一个包含 2 个元素的数组
var options = [, , , , ,] // 不推荐!创建包含空位的稀疏数组鉴于数组的常用性,ES6 专门扩展数组构造器 Array ,新增 2 个方法:Array.of 和 Array.from。Array.of 用得比较少,Array.from 具有很强的灵活性
Array.of
Array.of 可以把一组参数转换为数组。这个方法用于替代在 ES6 之前常用的 Array.prototype.slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:
console.log(Array.of(1, 2, 3, 4)) // [1, 2, 3, 4]
console.log(Array.of(undefined)) // [undefined]在下面的代码中,可以看到:当参数为 2 个时,返回的结果是一致的;当参数是一个时,Array.of 会把参数变成数组里的一项,而构造器则会生成长度和第一个参数相同的空数组
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的值,可选
// 字符串会被拆分为单字符数组
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 可以对现有数组执行浅复制
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1)
console.log(a1) // [1, 2, 3, 4]
alert(a1 === a2) // false// 可以使用任何可迭代对象
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 值在箭头函数中不适用
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 规范重新定义了该如何处理这些空位
const options = [, , , , ,] // 创建包含 5 个元素的数组
console.log(options.length) // 5
console.log(options) // [,,,,,]ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined:
const options = [1, , , , 5]
for (const option of options) {
console.log(option === undefined)
}
// false、true、true、true、false使用 ES6 的 Array.from 创建数组
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 之前的方法则会忽略这个空位
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。
let colors = ["red", "blue", "green"] // 定义一个字符串数组
alert(colors[0]) // 显示第一项
colors[2] = "black" // 修改第三项
colors[3] = "brown" // 添加第四项如果把一个值设置给超过数组最大索引的索引,就像示例中的 colors[3],则数组长度会自动扩展到该索引值加 1(示例中设置的索引 3,所以数组长度变成了 4)
let colors = new Array("red", "blue", "green")
colors.length = 2
console.log(colors[2]) // undefined
colors.length = 4
console.log(colors[3]) // undefinedlet colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
colors.length = 2
alert(colors[2]) // undefined如果将 length 设置为大于数组元素数的值,则新添加的元素都将以 undefined 填充:
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
colors.length = 4
alert(colors[3]) // undefined使用 length 属性可以方便地向数组末尾添加元素,如下例所示:
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
colors[colors.length] = "black" //添加一种颜色(位置3)
colors[colors.length] = "brown" //再添加一种颜色(位置4)colors 数组有一个值被插入到位置 99,结果新 length 就变成了 100。这中间的所有元素,即位置 3~98,实际上并不存在,因此在访问时会返回 undefined
let colors = ["red", "blue", "green"] // 创建一个包含 3 个字符串的数组
colors[99] = "black" //添加一种颜色(位置99)
alert(colors.length) // 100使用 at() 方法访问元素(ES2022)
ES2022 引入了 at() 方法,支持使用负数索引从数组末尾访问元素:
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)) // undefinedat() 方法的优势:
- 支持负数索引,访问末尾元素更方便
- 比
arr[arr.length - 1]更简洁 - 与字符串的
at()方法行为一致
检测数组 isArray
经典的 ECMAScript 问题是判断一个对象是不是数组。在只有一个网页(因而只有一个全局作用域)的情况下,使用 instanceof 操作符就够了:
if (value instanceof Array) {
// 操作数组
}使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
为解决这个问题,ECMAScript 提供 Array.isArray() 方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的
if (Array.isArray(value)) {
// 操作数组
}在 ES6 之前,至少有如下 5 种方式去判断一个对象是否为数组
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。
性能对比
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 主要用于对数组或者对象的属性进行循环操作。循环中的代码每执行一次,就会对对象的属性进行一次操作
const arr = [1, 2, 3]
for (var i in arr) {
console.log("键名:", i)
console.log("键值:", arr[i])
}
// 键名: 0
// 键值: 1
// 键名: 1
// 键值: 2
// 键名: 2
// 键值: 3for...of
for...of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of 循环,以替代 for...in 和 forEach,并支持新的迭代协议。for...of 允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。
该方法允许获取对象的键值:
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 等拥有迭代器对象的集合- 它可以正确响应
break、continue和return语句; for...of循环不支持遍历普通对象,因为没有迭代器对象。如果想要遍历一个对象的属性,可以用for-in循环
总结,for…of 和 for…in 的区别如下:
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | 键名(索引) | 键值(元素) |
| 原型链 | 会遍历原型链上的可枚举属性 | 只遍历当前对象 |
| 性能 | 较慢(需要检查原型链) | 较快 |
| 适用场景 | 对象属性遍历 | 数组、字符串、Map、Set 等可迭代对象 |
| 推荐度 | 不推荐用于数组 | 推荐用于数组 |
传统 for 循环
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 方法
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 提供了三个迭代器方法,可以获取数组的索引、值或键值对:
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,它的对象只定义在函数体中,包括了函数的参数和其他属性:
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 属性,指向当前正在执行的函数。
function foo(name, age, sex) {
console.log(arguments.callee)
}
foo("jack", "18", "male")打印结果如下:
ƒ foo(name, age, sex) {
console.log(arguments.callee);
}HTMLCollection 简单来说是 HTML DOM 对象的一个接口,这个接口包含获取到的 DOM 元素集合,返回的类型是类数组对象,如果用 typeof 来判断的话,它返回的是 object。它是及时更新的,当文档中的 DOM 变化时,它也会随之变化
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 也会随之变化
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 这个类数组的值,那么也可以对于参数进行一些操作
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 这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串,该参数指定一个字符作为衔接点来连接字符串
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"这段代码说明可以传递任意数量的参数到该函数,并使用每个参数作为列表中的项创建列表进行拼接。从这个例子中也可以看出,可以在日常编码中采用这样的代码抽象方式,把需要解决的这一类问题,都抽象成通用的方法,来提升代码的可复用性
可以借助 apply 或 call 与 arguments 相结合,将参数从一个函数传递到另一个函数
// 使用 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 方法
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 如何转换成数组:
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 方法 或 展开运算符的方法来将类数组转化为数组
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)。
密集数组
密集数组是指所有索引都有值的数组:
const dense = [1, 2, 3, 4, 5]
console.log(dense.length) // 5稀疏数组
稀疏数组是指某些索引位置没有元素的数组:
// 创建稀疏数组的方式
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 存在)检查数组元素是否存在
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)性能考虑
稀疏数组在某些操作中可能表现不佳:
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()
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() 方法可以将数组元素连接成字符串,可以指定分隔符:
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),可以方便地从数组中提取值:
// 基本解构
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)... 可以将数组展开为独立的元素:
// 展开数组
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()功能更强大
数组的性能优化建议
- 预分配数组大小:如果知道数组大小,可以预先设置
length - 避免稀疏数组:使用
undefined代替空位 - 选择合适的遍历方法:
for循环通常最快,forEach次之 - 避免频繁修改 length:直接修改
length可能触发性能问题 - 使用类型化数组:对于数值计算,考虑使用
TypedArray
// 性能优化示例
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: 如何判断一个变量是否为数组?
// ✅ 推荐方式
Array.isArray(arr)
// ❌ 不推荐的方式
arr instanceof Array // 在多框架环境下可能失效
arr.constructor === Array // constructor 可能被修改Q2: 数组和对象有什么区别?
| 特性 | 数组 | 对象 |
|---|---|---|
| 索引 | 数字索引 | 字符串键 |
| 顺序 | 有序 | 无序(ES2015+保持插入顺序) |
| length属性 | 自动维护 | 需手动维护 |
| 迭代 | for...of, 数组方法 | for...in, Object.keys()等 |
| 用途 | 有序数据集合 | 键值对存储 |
// 数组
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) // undefinedQ3: 如何避免数组方法修改原数组?
// 创建数组副本
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: 如何正确处理稀疏数组?
// ❌ 避免:创建稀疏数组
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, 4Q5: 如何选择合适的数组遍历方法?
快速选择指南:
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: 类数组对象和数组的区别是什么?
// 类数组对象
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. 数组创建
✅ 推荐做法:
// 使用字面量
const arr = [1, 2, 3]
// 使用 Array.from() 转换类数组
const arrayLike = {0: 'a', 1: 'b', length: 2}
const arr = Array.from(arrayLike)
// 使用扩展运算符复制数组
const copy = [...arr]❌ 避免做法:
// 构造函数的单参数歧义
const arr = new Array(3) // 创建长度为3的空数组
// 创建稀疏数组
const sparse = [1, , , 4]2. 数组遍历
✅ 推荐做法:
// 简单遍历
arr.forEach(item => console.log(item))
// 数据转换
const doubled = arr.map(x => x * 2)
// 需要退出循环时使用 for...of
for (const item of arr) {
if (condition) break
// 处理逻辑
}❌ 避免做法:
// 在 forEach 中使用 return 或 break
arr.forEach(item => {
if (condition) return // ❌ 只能跳过当前迭代,不能退出循环
})
// 使用 for...in 遍历数组
for (const index in arr) { // ❌ 会遍历原型链,不推荐用于数组
console.log(arr[index])
}3. 数组修改
✅ 推荐做法:
// 使用不可变操作(不修改原数组)
const newArr = [...arr, newItem]
const filtered = arr.filter(x => x > 0)
const mapped = arr.map(x => x * 2)❌ 避免做法:
// 直接修改原数组(除非明确需要)
arr.push(newItem)
arr.splice(0, 1)
// 在遍历中修改数组长度
arr.forEach((item, index) => {
if (condition) {
arr.splice(index, 1) // ❌ 可能导致意外行为
}
})4. 性能优化
✅ 推荐做法:
// 预分配数组大小
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. 代码可读性
✅ 推荐做法:
// 使用语义化的方法名
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 数组的基础知识:
- 核心特性:动态大小、类型灵活、对象本质
- 创建方式:字面量、构造函数、Array.of()、Array.from()
- 索引访问:数字索引、length属性、at()方法
- 数组检测:Array.isArray() 及其他方法
- 遍历方法:for循环、for...in、for...of、迭代器方法
- 类数组对象:arguments、NodeList及其转换
- 进阶特性:稀疏数组、解构赋值、扩展运算符
下一章预告:在数组方法中,我们将详细介绍数组的各种内置方法,包括增删改查、排序搜索、迭代处理等。
💡 学习建议:掌握数组基础是学习 JavaScript 的重要一步。建议多实践、多调试,理解数组方法的工作原理和性能特点。