
数组和字符串的API说实在的是前端开发里最容易被忽略却又最离不开的东西。你随便打开一个业务项目数据处理、表单校验、接口返回值的格式化、列表渲染十行代码里至少五六行是在跟数组和字符串打交道。我入行前几年也踩过不少坑有些问题当时查半天才明白其实本质上就是API用错了或者压根没用对。这篇内容不是把MDN文档搬过来念一遍而是把我这些年实际用下来觉得最值得掌握的常用API、容易混淆的点、以及能直接抄的高频场景完整梳理一遍。1. 为什么数组和字符串API值得系统过一遍1.1 开发中绕不开的两块基础能力很多刚入门的朋友会有一个误解觉得API嘛用的时候查一下就行没必要专门花时间系统学。这个想法在刚接触时确实没错但等你在真实项目里连续写上几个月就会发现后果很明显代码冗长、逻辑绕圈、bug频发而且排查问题的效率特别低。举个最简单的例子一个数组去重有人写三层for循环有人用Set加扩展运算符一行搞定这就是API熟练度带来的差距。数组和字符串本质上代表了编程里最常见的两种数据结构有序集合和有序字符序列。所有复杂的数据处理最终都可以拆解成这两种结构的操作组合。你掌握了数组的遍历、查找、过滤、归并再配合字符串的匹配、截取、替换、格式化基本就能应付日常绝大多数业务逻辑。我自己面试别人的时候也习惯从数组和字符串的API问起因为这两个点最能看出一个人是“背过文档”还是“真的在项目里写过”。1.2 怎么学才能记得住、用得对我的建议是不要按字母顺序去背而是按“用途”去分类记忆。比如数组操作先分清两种类型一类是直接修改原数组的像是push、pop、splice、reverse另一类是返回新数组、不碰原数据的像是map、filter、slice、concat。这个区分一旦建立起来你写代码时就不会再稀里糊涂地改坏了原数据。字符串也有一个核心知识点就是“字符串是不可变的”。所有字符串方法看起来是在“修改”字符串实际上都返回了一个新字符串原字符串毫发无损。理解了这两条主线再去看具体方法基本就不会觉得零散。我建议每个人都有自己的一份“API速查表”不用多全面但要把那些容易混淆的方法和自己在项目中真实用过的场景记在一起这样记忆才会牢固。2. 数组常用API分类详解与选型2.1 增删改操作先分清原地修改与返回新数组数组的增删改是基础中的基础但很多人一开始就在这里被绕晕。先说原地修改的一组push在末尾追加返回的是新数组长度pop删除末尾元素返回被删掉的元素unshift在头部添加返回新长度shift删除头部元素返回被删元素。这组方法最大的特点就是会直接改变原数组。const arr [1, 2, 3]; const length arr.push(4); // 返回 4arr 变成 [1, 2, 3, 4] const item arr.pop(); // 返回 4arr 变回 [1, 2, 3] arr.unshift(0); // 返回 4arr 变成 [0, 1, 2, 3] const first arr.shift(); // 返回 0arr 变成 [1, 2, 3]注意这里有个很容易被忽视的点push和unshift返回的是新数组的length不是添加进去的元素很多人写代码时顺手用这个返回值去取数组结果拿到一个数字排查半天才发现问题。再来说splice这是数组里“瑞士军刀”级别的存在一个方法就能搞定删除、插入、替换三种操作。它的签名是splice(start, deleteCount, ...items)const arr [1, 2, 3, 4, 5]; // 删除从索引 1 开始删 2 个 arr.splice(1, 2); // arr 变成 [1, 4, 5] // 插入从索引 1 开始删 0 个插入两个新元素 arr.splice(1, 0, a, b); // arr 变成 [1, a, b, 4, 5] // 替换从索引 1 开始删 1 个替换成新值 arr.splice(1, 1, x); // arr 变成 [1, x, a, 4, 5]这里有个小技巧splice的start也支持负数比如splice(-2, 1)就是从倒数第二个元素开始删一个。这在不知道数组长度、想从尾部操作时特别方便。和splice容易混淆的是slice它俩只有一个字母之差但行为完全不同。slice(start, end)不修改原数组而是返回一个从start到end不包含end的新数组。我遇到过不止一次同事写代码时想复制数组顺手用了splice结果原数组被改得面目全非。复制数组的正解是arr.slice()或者[...arr]。2.2 查找与判断方法选对方法比背对方法更重要查找是数组操作里最高频的场景之一。最基础的是indexOf和lastIndexOf返回元素第一次出现和最后一次出现的索引找不到就返回-1。这里有个经典坑就是判断逻辑很多人直接写if (arr.indexOf(item))这在对0号元素做判断时会出问题因为索引0在条件判断里是false。正确的判断写法是if (arr.indexOf(item) ! -1)或者干脆用includes。includes是ES2016加入的方法直接返回布尔值语义比indexOf更清晰。还有一个区别includes能正确识别NaN而indexOf不行。const arr [1, 2, NaN]; arr.indexOf(NaN); // -1找不到 arr.includes(NaN); // true能识别当查找条件不是简单的值相等而是要按某种规则找就要用find和findIndex。find返回第一个满足条件的元素本身findIndex返回索引。它们都接收一个回调函数回调返回true就表示命中。ES2023还加了findLast和findLastIndex从数组尾部开始找适合处理“最近一条记录”这类需求。const users [ { id: 1, name: A }, { id: 2, name: B }, ]; const target users.find(u u.id 2); // { id: 2, name: B } const index users.findIndex(u u.id 1); // 0还有一个判断类是some和every。some是“是否存在至少一个满足条件”every是“是否全部满足条件”。它们在表单校验场景里非常好用比如校验数组里是否所有字段都已填写。const scores [88, 92, 75, 60]; const hasPass scores.some(s s 90); // true至少有一个90分以上 const allPass scores.every(s s 60); // true全部及格2.3 遍历与转换方法map、filter、reduce的组合用法这三个方法放在一起说因为它们覆盖了数组处理最常见的三类需求一一转换、按条件筛选、归并计算。map把原数组的每个元素经过回调处理后返回一个等长度的新数组。它不会修改原数组而且回调的返回值会组成新数组的对应元素。最常见的用法是接口数据字段映射比如后端返回字段是userName前端展示需要name一行map就搞定。const list [{ userName: 张三 }, { userName: 李四 }]; const mapped list.map(item ({ name: item.userName })); // [{ name: 张三 }, { name: 李四 }]filter按条件筛选把回调返回true的元素收集到新数组里。常和map连用先筛后映射或者先映射后筛选根据具体需求来。一个典型场景是筛选出待办列表里未完成的任务。const tasks [ { title: 任务A, done: false }, { title: 任务B, done: true }, ]; const todo tasks.filter(t !t.done).map(t t.title); // [任务A]reduce是这三个里最强大的、也是新手最头疼的。它的核心逻辑是把整个数组“归并”成一个值。签名是reduce(callback, initialValue)回调接收四个参数累加器acc、当前元素cur、当前索引、原数组。回调的返回值会成为下一次回调的acc。如果不传initialValueacc会默认取第一个元素cur从第二个开始。const nums [1, 2, 3, 4]; const sum nums.reduce((acc, cur) acc cur, 0); // 10 const max nums.reduce((acc, cur) Math.max(acc, cur), -Infinity); // 4我用reduce最多的场景是把数组转成对象比如按id建索引const list [{ id: 1, name: A }, { id: 2, name: B }]; const map list.reduce((acc, item) { acc[item.id] item; return acc; }, {}); // { 1: { id: 1, name: A }, 2: { id: 2, name: B } }注意reduce回调里一定要返回acc不返回的话下一次回调拿到的就是undefined整个结果会被污染。这个错误我见过太多次了是所有reduce相关bug的头号来源。2.4 排序、扁平化与去重高频场景的实战组合数组的sort方法默认会把元素转成字符串再排序这导致一个经典问题[10, 9, 100].sort()的结果是[10, 100, 9]因为字符串比较时10小于9。正确的做法永远是传一个比较函数。const nums [10, 9, 100]; nums.sort((a, b) a - b); // [9, 10, 100]升序 nums.sort((a, b) b - a); // [100, 10, 9]降序比较函数返回负值表示a排在b前正值表示a排在b后0表示相等。数字排序直接做减法就行但对象数组按某个字段排序时要记得取出对应字段再比较。对象数组按日期字段排序是我常遇到的场景const orders [ { id: 1, date: 2024-03-01 }, { id: 2, date: 2024-01-15 }, { id: 3, date: 2024-05-20 }, ]; orders.sort((a, b) a.date.localeCompare(b.date));这里用localeCompare是因为它可以直接比较字符串的字典顺序对日期格式YYYY-MM-DD来说就是正确的时间顺序。数组扁平化现在有原生方法flat可以指定展开层数默认是1层。如果需要完全展开传Infinity。flatMap则是map之后再做一层flat的组合两者配合可以优雅处理嵌套结构。const nested [[1, 2], [3, [4]]]; nested.flat(); // [1, 2, 3, [4]] nested.flat(Infinity); // [1, 2, 3, 4] const items [a, b]; items.flatMap(x [x, x.toUpperCase()]); // [a, A, b, B]去重最简洁的方法是借助Setconst arr [1, 2, 2, 3, 3, 4]; const unique [...new Set(arr)]; // [1, 2, 3, 4]但要注意Set去重用是SameValueZero比较规则对于对象数组去重相同id但不同引用是不行的得配合reduce或filter按字段去重。const users [ { id: 1, name: A }, { id: 1, name: B }, { id: 2, name: C }, ]; const seen new Set(); const uniqueUsers users.filter(u { if (seen.has(u.id)) return false; seen.add(u.id); return true; });3. 字符串常用API分类详解与实战3.1 查找与匹配从indexOf到matchAll字符串的查找方法和数组很相似indexOf返回子串首次出现的位置找不到返回-1includes直接返回布尔值。还有一对容易被忽略的startsWith和endsWith在判断前缀后缀时特别方便比如判断文件名扩展名、判断URL协议等。const filename report_2024.pdf; filename.endsWith(.pdf); // true filename.startsWith(report); // true filename.includes(2024); // true如果要从字符串里提取符合某种模式的内容就需要search、match和matchAll。search返回匹配正则的第一个位置相当于indexOf的正则版match返回第一个匹配的详细信息全局匹配时要用matchAll它返回一个迭代器。const text 订单号A001金额¥100; const matched text.match(/[A-Z]\d{3}/); // matched[0] 是 A001 const prices 价格: 12元、20元、35元; const iterator prices.matchAll(/(\d)元/g); for (const m of iterator) { console.log(m[1]); // 12 20 35 }这里有个实际经验matchAll必须配合正则的g标志使用否则会抛异常。而且在循环里使用正则对象时要注意lastIndex的状态问题用迭代器的方式会安全很多。3.2 截取与拼接slice、substring、substr别再混淆字符串截取有三个方法名字相似但行为差异挺大这组是我认为最值得反复强调的混淆点。slice(start, end)和substring(start, end)都返回从start到end不含end的子串区别在于slice支持负索引从末尾倒数substring遇到负数会当作0处理。const str hello world; str.slice(0, 5); // hello str.slice(-5); // world str.substring(0, 5); // hello str.substring(-5); // 等价于 substring(0)返回整个字符串substr(start, length)是第三个方法第二个参数是长度而不是结束位置。这个方法在规范里已经被标记为废弃但老项目里仍大量存在。接手旧代码时看到它建议重构为slice。const str hello world; str.substr(6, 5); // world拼接字符串最常用的就是操作符简单直观。concat方法虽然存在但在现代JavaScript中没什么必要用因为的语义更清晰、性能也更好。模板字符串是ES6之后最推荐的拼接方式尤其在变量多、带换行的场景里可读性完全碾压号连写。const name 张三; const age 28; const info ${name}今年${age}岁;split是字符串转数组的核心方法方向正好和join相反。它支持传正则做分隔符也支持限制结果长度。const csv a,b,c,d; csv.split(,); // [a, b, c, d] csv.split(,, 2); // [a, b] const raw 2024-03-01; const parts raw.split(-); // [2024, 03, 01]3.3 格式处理与补全trim、padStart这些容易被忽略的好用方法trim系列是老生常谈了但很多人只记得trim()不知道ES2019加入了trimStart()和trimEnd()。做输入框校验时只清首部或尾部空格的需求是真实存在的用对了能少写不少replace正则。大小写转换有toUpperCase和toLowerCase还有个不常用的toLocaleUpperCase和toLocaleLowerCase后者会更尊重当前语言环境的规则比如土耳其语的i大小写转换和英语不同。日常业务我基本用前者就够了。repeat方法复制字符串指定次数生成占位符时会用上const placeholder -.repeat(20);最容易被低估的是padStart和padEnd。它们的作用是把字符串补足到指定长度补全内容可以自定义。典型的场景是时间格式化需要把1补成01const hour 9; hour.toString().padStart(2, 0); // 09我自己在生成编号、订单号、序号时也经常用比如把编号统一补到8位const no 123; String(no).padStart(8, 0); // 00000123charAt和charCodeAt在通俗场景里用得不多但处理字符串逐字符遍历时会有用。charAt取指定位置的字符at方法则是ES2022加入的支持负索引语义更统一const str abc; str.charAt(1); // b str.at(-1); // c3.4 替换与模板字符串replaceAll和正则的配合replace老版本有一个让人头疼的特点第一个参数如果是字符串只替换第一个匹配项。想要全局替换必须用正则加g标志const str a-b-c; str.replace(-, ); // ab-c只替换第一个 str.replace(/-/g, ); // abc全局替换ES2021加入了replaceAll直接用字符串就能全局替换不需要正则了str.replaceAll(-, ); // abcreplace的第二个参数不只是字符串还可以是回调函数回调接收匹配到的内容返回值会用作替换结果。这个特性在处理动态替换时非常有用典型场景是给关键词加高亮const text 今天的课程讲了JavaScript和TypeScript; const keywords [JavaScript, TypeScript]; const highlighted keywords.reduce( (acc, kw) acc.replaceAll(kw, strong${kw}/strong), text );模板字符串除了基本拼接还有一个“标签模板”的高级玩法我记得在封装日志方法时用过不过日常业务用得少知道有这个概念就行。4. 三个高频业务场景的完整拆解4.1 数据清洗与数组去重后端接口返回的数据往往不能直接用于渲染需要在前端做一层清洗。最常见的问题包括空值过滤、字段映射、嵌套结构展开、去重。我举一个综合点的例子假设接口返回了一个用户列表其中有些用户是重复的有些用户的邮箱字段为空需要在渲染前处理干净。const rawList [ { id: 1, name: A, email: aexample.com }, { id: 1, name: A, email: aexample.com }, { id: 2, name: B, email: }, { id: 3, name: C, email: null }, ]; const seen new Set(); const cleanList rawList .filter(u u.email) // 过滤掉空邮箱 .filter(u { if (seen.has(u.id)) return false; seen.add(u.id); return true; }) .map(u ({ ...u, email: u.email.toLowerCase(), // 统一转小写 }));这个流程的思路是先做条件过滤再去重最后做字段格式化。每一步用一个方法职责单一逻辑也好排查。如果某一步数据不对直接看对应的filter或map就行。4.2 URL参数解析与拼接URL参数处理是前后端联调中的高频需求。把查询字符串?namezhangsanage28转成对象我常用URLSearchParams但如果只是批量处理参数字符串API也能搞定。function parseQuery(queryString) { if (!queryString) return {}; const str queryString.startsWith(?) ? queryString.slice(1) : queryString; return str.split().reduce((acc, pair) { const [key, value] pair.split(); if (key) { acc[decodeURIComponent(key)] decodeURIComponent(value || ); } return acc; }, {}); } const params parseQuery(?name张三age28city); // { name: 张三, age: 28, city: }反向拼接对象为查询字符串的流程相反function buildQuery(params) { return Object.entries(params) .filter(([, v]) v ! undefined v ! null) .map(([k, v]) ${encodeURIComponent(k)}${encodeURIComponent(v)}) .join(); } buildQuery({ name: 张三, age: 28, empty: }); // name%E5%BC%A0%E4%B8%89age28empty这里我特别强调编码问题手动拼接URL时任何可能含中文或特殊字符的字段都应该经过encodeURIComponent处理否则会生成非法URL。4.3 列表渲染与字段格式化前端的列表渲染大头是表格和卡片。后端返回的时间戳、状态码、金额数字通常不能直接展示要经过格式化。这种格式化操作本质上就是字符串和数组的API串联。const rows [ { id: 1, createdAt: 1710000000000, amount: 1234.5, status: paid }, { id: 2, createdAt: 1710003600000, amount: 3000, status: pending }, ]; const formatted rows.map(row ({ ...row, dateText: new Date(row.createdAt).toLocaleString(zh-CN), amountText: ¥${row.amount.toFixed(2)}, statusText: row.status paid ? 已支付 : 待支付, }));注意amount.toFixed(2)返回的是字符串所以amountText天然是字符串类型直接渲染到页面没有问题。而dateText的生成本质上是先把时间戳转换成Date对象再调用字符串格式化方法这就是字符串API在业务中的实际位置。5. 常见问题与踩坑记录5.1 修改原数组与返回新数组的对比速查表这是我认为最值得收藏的一张表因为很多莫名其妙的bug源头就在这里。方法是否修改原数组返回值典型误用场景push / pop / unshift / shift修改新长度或删除的元素误以为返回新数组splice修改被删除的元素数组误以为返回修改后的数组reverse修改原数组已被反转误以为不修改原数组sort修改原数组已排序误以为返回新数组slice不修改新数组和splice混淆concat不修改新数组忽略展开嵌套数组map不修改新数组用map做副作用操作filter不修改新数组回调条件写反reduce不修改归并结果忘记返回accflat不修改新数组层数未注意我特别想强调一下reverse和sort它们会直接修改原数组并且返回同一个数组这意味着你如果想让变量b等于排序后的新数组结果a和b指向的还是同一个引用原来的a已经变了。想要保留原数组正确做法是先用[...arr]复制一份再排序。const original [3, 1, 2]; const sorted [...original].sort((a, b) a - b); // original 还是 [3, 1, 2]sorted 是 [1, 2, 3]ES2023加入的toSorted、toReversed、toSpliced这三个方法就是专门解决这个痛点的它们不修改原数组直接返回新数组。如果项目能使用新特性给数组做排序和反转时优先用它们代码会更安全。5.2 回调函数参数顺序的坑数组方法里的回调函数参数顺序统一是(当前元素, 当前索引, 原数组)。这个顺序本身不复杂坑主要出现在你只想用第二个参数的时候。比如有人想用findIndex却写成了arr.findIndex((index) ...)回调里拿到的第一个参数其实是元素值不是索引。这个问题的根源是“只传需要的参数”这个习惯在回调里不适用回调的参数位置是固定的不用的参数可以用下划线占位但不能跳过前面的直接写后面的。另一个容易出错的点是sort和reduce的回调它们的参数顺序完全不同于map、filter那组。sort回调是(a, b)reduce回调是(acc, cur, index, array)写错顺序的结果不是报错而是逻辑悄无声息地出错排查起来非常耗时间。5.3 隐式类型转换与NaN判断数组和字符串方法在遇到类型不匹配时不会主动报错而是做隐式转换。最典型的例子是数字数组和字符串混用。const nums [1, 2, 3]; nums.includes(2); // false严格相等比较 nums.indexOf(2); // -1严格相等比较但如果你把数字和字符串相加又会得到拼接结果而不是数值结果。这就是混合数据处理时最容易出现的隐性bug。我的经验是从接口拿到的数据先明确类型再操作必要时用Number()或String()统一转换。还有NaN的识别问题。前面提到includes能识别NaNindexOf不能。如果你一定要在数组里查找NaN用find加Number.isNaN做判断最稳妥。const arr [1, NaN, 3]; arr.find(item Number.isNaN(item)); // NaN这里的核心原因是indexOf和includes使用了不同的相等性算法find则是走回调可以自定义比较逻辑。5.4 性能注意与ES新方法日常业务开发里数组和字符串方法处理几百甚至几千条数据性能差异基本感知不到。真要注意性能的地方反而是方法本身的复杂度特征。比如在超大数组上频繁使用unshift因为需要移动所有元素代价很高splice删除头部元素同理。如果要做第一版队列式结构用一个对象模拟或者直接用内置Queue思路会更合适。还有一点是链式调用不要无节制地串联。每个方法都会产生一次数组遍历arr.filter().map().reduce()会把数据完整遍历三遍。数据量小无所谓几万条以上时如果可能可以用reduce一次搞定多步操作或者在map里同时完成过滤用flatMap配合空数组[]丢弃不需要的元素。这是性能优化的一个思路。// 原来写法两次遍历 const result arr.filter(x x 10).map(x x * 2); // 单次遍历等价写法 const result arr.flatMap(x x 10 ? [x * 2] : []);新版ES还带来了一些值得关注的方法比如Object.groupBy支持按字段分组做统计分组时能少写不少代码const people [ { name: A, age: 20 }, { name: B, age: 30 }, { name: C, age: 20 }, ]; const byAge Object.groupBy(people, p p.age); // { 20: [{ name: A, age: 20 }, { name: C, age: 20 }], 30: [{ name: B, age: 30 }] }数组的at方法、with方法也值得纳入工具箱。with(index, value)返回一个新数组替换指定位置的值而不改原数组配合不可变数据流的思路很顺手const arr [1, 2, 3]; const newArr arr.with(1, 99); // [1, 99, 3] // arr 还是 [1, 2, 3]关于这些ES新方法我现在实际项目里的用法是如果项目跑在比较新的运行时环境优先使用新方法如果是老项目、有兼容性要求就继续用老方法并注意复制数组。不追求一定要用最前沿的API关键是先把手头代码写对。结尾数组和字符串的API数量看着多其实用久了就会发现就那么几十个方法在反复出现。我个人最深的体会是与其把每个方法的细节都背下来不如先把几个核心分类搞清楚原地修改还是返回新数组、参数顺序是什么、回调返回值要什么。这三条主线清楚了就算某个方法签名记不准确手边查一下文档也能立刻用对。真正常踩的坑往往是对这些主线的理解偏差而不是具体的API用法。如果这篇内容对你有帮助建议自己动手把文中几个高频场景的代码都跑一遍再想想项目中哪些地方可以替换成更优雅的写法。等你能把数组和字符串的方法融合进一个复杂数据处理流程里那时候你再看老代码就会发现很多地方都可以简化成三五行。