- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
数组是 JavaScript 中最常用的数据结构之一,而Array.prototype上挂载的方法多达数十种。本篇以《现代 JavaScript 教程》中 数组方法 一章为骨架,系统讲解增删元素(splice/slice/concat)、遍历(forEach)、搜索(indexOf/find/filter)、转换(map/sort/split/join/reduce)等核心方法,并结合本仓库 05-array-methods 目录下 12 道配套习题与参考实现,帮助你把每一个方法用对、用活。读完本文,你将掌握一份可随时查阅的数组方法备忘单,并具备独立解决"字符串转驼峰、范围过滤、数组去重、按字段排序、按 id 分组"等日常开发问题的能力。
添加/移除数组元素
数组的四把"入门工具"我们在前面的章节已经接触过,这里先复习:
arr.push(...items)—— 从尾端添加元素;arr.pop()—— 从尾端提取(删除并返回)元素;arr.shift()—— 从首端提取元素;arr.unshift(...items)—— 从首端添加元素。
它们只操作数组两端。如果你需要在任意位置增删元素,就需要下面几个进阶方法。
splice:数组的"瑞士军刀"
直接使用delete删除数组元素会留下"空洞":
let arr = ["I", "go", "home"]; delete arr[1]; // 删除 "go" alert( arr[1] ); // undefined alert( arr.length ); // 3(元素还在,只是变空了)原因在于delete obj.key只是移除键对应的值,数组作为对象并不会因此收缩长度。想要让后面的元素前移、得到一个更短的数组,必须使用专门的方法——arr.splice正是为此而生,它可以删除、插入、替换一气呵成,因此常被称为数组方法中的"瑞士军刀"。
语法:
arr.splice(start[, deleteCount, elem1, ..., elemN])它从索引start开始修改arr:删除deleteCount个元素,并在当前位置插入elem1, ..., elemN,最后返回被删除元素组成的数组。
看几个实例:
// 1) 从索引 1 开始删除 1 个元素 let arr = ["I", "study", "JavaScript"]; arr.splice(1, 1); alert( arr ); // ["I", "JavaScript"] // 2) 删除前 3 个元素,并用另外两个元素替换它们 let arr2 = ["I", "study", "JavaScript", "right", "now"]; arr2.splice(0, 3, "Let's", "dance"); alert( arr2 ); // ["Let's", "dance", "right", "now"] // 3) 返回值是被删除的元素组成的数组 let arr3 = ["I", "study", "JavaScript", "right", "now"]; let removed = arr3.splice(0, 2); alert( removed ); // "I", "study" // 4) deleteCount 为 0 时,只插入不删除 let arr4 = ["I", "study", "JavaScript"]; arr4.splice(2, 0, "complex", "language"); alert( arr4 ); // "I", "study", "complex", "language", "JavaScript"负向索引:
splice及本章多数方法都允许负索引,从数组末尾倒数计算位置。例如arr.splice(-1, 0, 3, 4)表示"从倒数第 1 位(尾端前一位)开始,删除 0 个元素,插入 3 和 4",结果是[1, 2, 3, 4, 5]。
实战佐证:习题 3-filter-range-in-place 要求原地删除范围之外的元素,其 参考实现 正是利用splice删除后回退下标i--,避免跳过元素:
function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { let val = arr[i]; if (val < a || val > b) { // 超出 [a, b] 范围则删除 arr.splice(i, 1); i--; // 删除后索引前移,回退一位再检查 } } } let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); alert( arr ); // [3, 1]注意该函数只修改数组、不返回任何东西,这是"原位操作"类 API 的典型约定。
slice:复制子数组
arr.slice([start], [end])比splice简单得多:它返回一个新数组,包含从索引start到end(不含end)的所有元素。start和end都可以是负数(从末尾计数)。它和字符串的str.slice用法神似,只不过操作对象是数组。
let arr = ["t", "e", "s", "t"]; alert( arr.slice(1, 3) ); // e,s(复制位置 1 到 3,不含 3) alert( arr.slice(-2) ); // s,t(复制倒数第 2 位到尾端)不带参数调用arr.slice()会得到整个数组的浅拷贝,这是"先复制再转换、不动原数组"模式的常用起点。
concat:拼接数组与其他值
arr.concat(arg1, arg2...)接受任意数量的参数(数组或单个值),返回一个新数组:先是arr的所有元素,随后依次是各参数的元素(参数为数组时展开其元素,否则直接追加参数本身)。
let arr = [1, 2]; alert( arr.concat([3, 4]) ); // 1,2,3,4 alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6类数组对象的特殊行为:默认情况下,普通对象即使带length属性(如{0: "something", length: 1})也会被当作单个整体追加进去,得到1,2,[object Object]。但如果该对象带有Symbol.isConcatSpreadable属性且值为true,它就会被concat当作数组展开:
let arrayLike = { 0: "something", 1: "else", [Symbol.isConcatSpreadable]: true, length: 2 }; alert( arr.concat(arrayLike) ); // 1,2,something,else遍历:forEach
arr.forEach(function(item, index, array) {...})为数组的每个元素调用一次函数,最常用于"只遍历、不求返回值"的场景:
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);回调可以拿到三个参数——当前元素item、索引index、数组本身array:
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); });回调的返回值会被forEach忽略;如果你需要在遍历过程中产出新值,请改用后面的map。
在数组中搜索
indexOf / lastIndexOf / includes
三者与字符串同名方法行为一致,只是作用对象是数组元素:
arr.indexOf(item, from)—— 从索引from开始搜索item,找到返回索引,否则返回-1;arr.lastIndexOf(item, from)—— 与indexOf相同,但从右向左查找;arr.includes(item, from)—— 从索引from开始搜索,找到返回true,否则返回false。
let arr = [1, 0, false]; alert( arr.indexOf(0) ); // 1 alert( arr.indexOf(false) ); // 2 alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true let fruits = ['Apple', 'Orange', 'Apple']; alert( fruits.indexOf('Apple') ); // 0(第一个 Apple) alert( fruits.lastIndexOf('Apple') ); // 2(最后一个 Apple)两个注意点:
- 使用严格相等
===比较:搜索false只会命中布尔false,不会命中数字0。 includes能正确处理NaN,而indexOf不能:
const arr = [NaN]; alert( arr.indexOf(NaN) ); // -1(错误,应为 0) alert( arr.includes(NaN) ); // true(正确)这是因为includes较晚加入 JavaScript,内部采用了更新、更完善的比较算法。只关心"是否存在"、不关心索引时,includes是首选。
实战佐证:习题 11-array-unique(数组去重)的 参考实现 正是用includes判断元素是否已存在于结果集:
function unique(arr) { let result = []; for (let str of arr) { if (!result.includes(str)) { result.push(str); } } return result; } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O"]; alert( unique(strings) ); // Hare, Krishna, :-O该解法正确但存在性能隐患:includes内部要线性扫描result,若arr有 10000 个元素,最坏会达到 1 亿次比较,仅适合小型数组。更优方案(利用Set)会在本教程 Map 和 Set 一章中给出。
find / findIndex / findLastIndex:按条件查找
对象数组在实际开发中极其常见,这时需要按条件而非按值查找。arr.find依次对每个元素调用回调:
let result = arr.find(function(item, index, array) { // 返回 true 则停止迭代并返回 item;全部为假则返回 undefined });回调参数:item(元素)、index(索引)、array(数组本身)。找到第一个满足条件的元素即返回,否则返回undefined。
let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; let user = users.find(item => item.id == 1); alert(user.name); // John配套方法:
arr.findIndex—— 语法相同,但返回索引而非元素,未找到返回-1;arr.findLastIndex—— 与findIndex相同,但从右向左搜索,类似lastIndexOf。
let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"}, {id: 4, name: "John"} ]; alert(users.findIndex(user => user.name == 'John')); // 0(第一个 John) alert(users.findLastIndex(user => user.name == 'John')); // 3(最后一个 John)filter:筛选所有匹配项
find只返回第一个匹配元素;要拿到所有匹配元素组成的数组,使用arr.filter(fn)。语法与find类似,但回调返回true时元素被收集进结果数组,迭代继续;没有匹配时返回空数组。
let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; let someUsers = users.filter(item => item.id < 3); alert(someUsers.length); // 2实战佐证:习题 2-filter-range(过滤范围)要求返回新数组且不修改原数组,其 参考实现 一行搞定:
function filterRange(arr, a, b) { return arr.filter(item => (a <= item && item <= b)); } let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1(匹配的值) alert( arr ); // 5,3,8,1(原数组未被改动)对比前面用splice的filterRangeInPlace,可以清晰看到"返回新数组"(filter)与"原位修改"(splice)两种编程风格的分野。
转换数组
map:一对一映射
arr.map是最常用、最有用的方法之一:对每个元素调用回调,收集返回值组成新数组,原数组不变。
let result = arr.map(function(item, index, array) { // 返回新值替换当前元素 });let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6实战佐证:习题 1-camelcase(将border-left-width转为borderLeftWidth)是split → map → join三步流的教科书案例,其 参考实现 为:
function camelize(str) { return str .split('-') // 'my-long-word' -> ['my', 'long', 'word'] .map( // 除第一项外,其余各项首字母大写 (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) ) .join(''); // ['my', 'Long', 'Word'] -> 'myLongWord' } camelize("background-color") === 'backgroundColor'; // true camelize("list-style-image") === 'listStyleImage'; // true camelize("-webkit-transition") === 'WebkitTransition'; // truesort(fn):原位排序
arr.sort()对数组进行原位(in-place)排序——直接改变arr自身顺序,并返回排序后的数组(返回值通常被忽略)。关键陷阱:默认按字符串的词典顺序排序!
let arr = [ 1, 2, 15 ]; arr.sort(); alert( arr ); // 1, 15, 2(字符串比较下 "2" > "15")要按自己的规则排序,需要传入比较函数compare(a, b),它返回:
function compare(a, b) { if (a > b) return 1; // 第一个值更大 if (a == b) return 0; // 相等 if (a < b) return -1; // 第一个值更小 }按数字升序排序:
let arr = [ 1, 2, 15 ]; arr.sort((a, b) => a - b); alert(arr); // 1, 2, 15关于sort的几个要点:
- 比较函数只需返回正数/负数/零,不必严格返回 ±1。因此
(a, b) => a - b是最简洁的数字升序写法,箭头函数版本与完整函数完全等价(回顾 箭头函数基础)。 - 内部采用通用排序算法(多数引擎实现为快速排序或 Timsort 的优化版本),会尽可能减少比较次数;可以通过在比较函数中
alert观察算法实际比较了哪些元素。 - 字符串排序建议用
localeCompare:默认字符串比较按字符编码进行,对Ö这类字母会出错。用str.localeCompare才能得到符合语言习惯的顺序:
let countries = ['Österreich', 'Andorra', 'Vietnam']; alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich(错的) alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra, Österreich, Vietnam(对的!)(关于字符串比较算法详见 字符串 一章。)
实战佐证:习题 8-sort-objects(按年龄对用户排序)演示了对象数组 +sort的标准组合:
let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; arr.sort((a, b) => a.age - b.age); alert(arr[0].name); // John(25 岁) alert(arr[1].name); // Mary(28 岁) alert(arr[2].name); // Pete(30 岁)reverse:原地反转
arr.reverse()颠倒arr中元素的顺序(原位操作),并返回反转后的数组:
let arr = [1, 2, 3, 4, 5]; arr.reverse(); alert( arr ); // 5,4,3,2,1split 和 join:字符串 ⇄ 数组
想象一个真实场景:消息应用里用户输入了逗号分隔的收件人列表"John, Pete, Mary",而业务逻辑需要的是名字数组。str.split(delim)通过分隔符delim把字符串切分成数组:
let names = 'Bilbo, Gandalf, Nazgul'; let arr = names.split(', '); for (let name of arr) { alert( `A message to ${name}.` ); // A message to Bilbo(依次类推) }split还有可选的第二个数字参数限制结果数组长度,超出部分被忽略(实际使用较少):
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2); alert(arr); // Bilbo, Gandalf用空字符串split('')可以把字符串拆成字符数组:
let str = "test"; alert( str.split('') ); // t,e,s,tarr.join(glue)是split的逆操作:用glue把数组项粘合成字符串:
let arr = ['Bilbo', 'Gandalf', 'Nazgul']; let str = arr.join(';'); alert( str ); // Bilbo;Gandalf;Nazgul实战联动:上面
camelize习题正是split('-') → map → join('')的完整链条,可见这三个方法经常组合出招。
reduce / reduceRight:归约为单个值
当需要遍历并返回每个元素的数据时用map;当需要根据整个数组计算单个值时用arr.reduce。语法:
let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]);回调被逐个应用到每个元素,并把结果"搬运"到下一次调用:
accumulator—— 上一次调用的结果,第一次等于initial(若提供);item—— 当前元素;index—— 当前索引;array—— 数组本身。
求数组总和:
let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15执行过程(初始值0作为第一次调用的sum):
| 调用次数 | sum | current | result |
|---|---|---|---|
| 第 1 次 | 0 | 1 | 1 |
| 第 2 次 | 1 | 2 | 3 |
| 第 3 次 | 3 | 3 | 6 |
| 第 4 次 | 6 | 4 | 10 |
| 第 5 次 | 10 | 5 | 15 |
也可以省略初始值:此时reduce把数组第一个元素当作初始值,从第二个元素开始迭代,结果同样为15。但空数组 + 无初始值会直接抛错(TypeError: Reduce of empty array with no initial value),因此实践上建议始终显式指定初始值。
arr.reduceRight与reduce完全一致,只是遍历方向为从右到左。
实战佐证:习题 12-reduce-object(以id为键把用户数组转成对象)是reduce的高级用法——累加器不是数字而是对象,其 参考实现 为:
function groupById(array) { return array.reduce((obj, value) => { obj[value.id] = value; // 以 id 为键,数组项为值 return obj; // 必须返回累加器,供下一次调用使用 }, {}) // 初始值必须是空对象 } let users = [ {id: 'john', name: "John Smith", age: 20}, {id: 'ann', name: "Ann Smith", age: 24}, {id: 'pete', name: "Pete Peterson", age: 31}, ]; let usersById = groupById(users); // usersById = { john: {...}, ann: {...}, pete: {...} }处理服务端返回的用户列表时,这种"数组 → 以 id 索引的对象"转换能极大简化后续查找。该任务假设id唯一;若可能出现重复键,需要先约定合并策略。
Array.isArray:识别数组
数组基于对象实现,并不构成独立的语言类型,所以typeof无法区分数组和普通对象:
alert(typeof {}); // object alert(typeof []); // object(相同!)判断是否为数组要用专门方法Array.isArray(value):
alert(Array.isArray({})); // false alert(Array.isArray([])); // true大多数方法都支持 thisArg
几乎所有"调用函数"的数组方法——find、filter、map等(sort是例外)——都接受一个可选的最后一个参数thisArg,它的值会成为回调内部的this:
arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg);典型用法:把对象方法直接当作回调,同时保持其this指向:
let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30} ]; // 找到 army.canJoin 返回 true 的 user let soldiers = users.filter(army.canJoin, army); alert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23若写成users.filter(army.canJoin),canJoin会被当作独立函数调用,此时this === undefined,访问this.minAge会立即报错。等效且更易读的写法是users.filter(user => army.canJoin(user))——实践中后者更常见,因为箭头函数天然捕获外层this,无需显式传thisArg。
补充方法:some / every / fill / copyWithin / flat / Array.of
除上述主力方法外,还有几个值得收录的补充方法(完整清单可查阅 MDN 的 Array 手册):
arr.some(fn)/arr.every(fn)—— 对每个元素调用fn,若任一/全部结果为真则返回true,否则返回false。行为类似||与&&:some遇到真值立即短路返回true,every遇到假值立即短路返回false。经典应用是比较两个数组:
function arraysEqual(arr1, arr2) { return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]); } alert( arraysEqual([1, 2], [1, 2])); // truearr.fill(value, start, end)—— 用value填充从索引start到end的区间(原位操作)。arr.copyWithin(target, start, end)—— 把从start到end的元素复制到数组自身的target位置(覆盖现有元素)。arr.flat(depth)/arr.flatMap(fn)—— 从多维数组创建扁平化新数组;flatMap等价于map后再flat(1)。Array.of(element0, element1, ...)—— 基于可变数量参数创建新的Array实例,不受参数数量或类型限制。
总结:数组方法备忘单
添加/删除元素
push(...items)—— 向尾端添加;pop()—— 从尾端提取一个;shift()—— 从首端提取一个;unshift(...items)—— 向首端添加;splice(pos, deleteCount, ...items)—— 从pos开始删除deleteCount个元素并插入items;slice(start, end)—— 返回新数组,复制[start, end)区间;concat(...items)—— 返回新数组:复制当前数组全部元素并追加items(数组参数展开元素)。
搜索元素
indexOf/lastIndexOf(item, pos)—— 从pos开始搜索,找到返回索引,否则返回-1;includes(value)—— 存在则返回true(可正确处理NaN);find/filter(func)—— 返回使func为真的第一个值 / 所有值;findIndex—— 同find,但返回索引;findLastIndex从右向左找。
遍历元素
forEach(func)—— 对每个元素调用func,无返回值。
转换数组
map(func)—— 根据回调返回值创建新数组;sort(func)—— 原位排序(默认按字符串!);reverse()—— 原位反转;split/join—— 字符串拆数组 / 数组拼字符串;reduce/reduceRight(func, initial)—— 迭代归约为单个值(建议始终给初始值)。
其他
Array.isArray(value)—— 判断是否为数组。
请注意:sort、reverse和splice修改的是数组本身;slice、map、filter、concat、reduce返回新数组/新值,不改动原数组。
这些方法覆盖了约 99% 的日常用例。记忆方法:先浏览上面的备忘单建立印象,再动手完成 05-array-methods 目录下的 12 道习题(字符串转驼峰、范围过滤与原地过滤、数组去重、按年龄排序、可扩展计算器、按 id 分组等),遇到"不知道该用什么方法"的操作时回到这份备忘单对照示例——用不了太久,你就能不假思索地选出正确方法。
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
现代 JavaScript 教程:Object.keys / values / entries 遍历与转换实战指南
现代 JavaScript 教程:Object.keys / values / entries 遍历与转换实战指南 本文以现代 JavaScript 教程(zh
文档教程前端千古前端图文教程:JavaScript 数组的常见方法全解析(类型判断、增删改查、遍历与排序)
千古前端图文教程:JavaScript 数组的常见方法全解析(类型判断、增删改查、遍历与排序) 本篇文章来自「千古前端图文教程」仓库的《JavaScript 基
文档教程前端The JavaScript Way 数组实战指南:用 Array 存储、访问、遍历与增删数据(第 7 章精讲)
The JavaScript Way 数组实战指南:用 Array 存储、访问、遍历与增删数据(第 7 章精讲) 本文基于开源书籍《The JavaScript
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考