JSON 数组这个知识点,看起来简单,但我在实际项目里见过太多人在这里栽跟头。比如从接口拿回一个数组,想取第三个元素,结果下标从 0 开始数,直接取到了隔壁老王;再比如后端返回的嵌套数组,前端同学遍历的时候一脸懵,不知道该怎么把内层数据捞出来。所以今天我不讲那些教科书式的大道理,就从一个实际开发者的角度,把 JSON 数组的访问、遍历、嵌套、修改、删除这五件事彻底梳理一遍。看完这篇,你至少能少踩一半的坑。
这篇内容主要适合这几类人:刚接触前后端联调的新人,写脚本处理 JSON 数据的测试或运维同学,以及偶尔要跟 JSON 打交道的全栈开发者。我会用 JavaScript 和 Python 两种语言对照演示,因为这两者在 JSON 处理上最常用,其他语言思路都类似。
1. JSON 数组基础认知:先搞清楚它到底是什么
1.1 数组在 JSON 结构里的位置
JSON 只有两种复合结构:对象(花括号括起来的一组键值对)和数组(方括号括起来的一组有序值)。在这两种结构里,数组又显得更特殊一些——对象有键名,数组没有键名,只有位置编号,也就是索引。
举个例子,一个标准的 JSON 数组长这样:
["苹果", "香蕉", "橙子"]这背后包含两层信息:第一,它是一个数组;第二,数组里有三个字符串元素。在实际业务里,数组里最常见的不是字符串,而是一堆对象。比如一个用户列表:
[ {"id": 1, "name": "张三"}, {"id": 2, "name": "李四"} ]这种“数组包对象”的组合,才是开发中接触最高频的结构。很多新手一看到方括号里有花括号就懵了,其实本质很简单:数组就是一个“装东西的盒子”,里面可以装任何 JSON 支持的数据类型,包括对象、字符串、数字、布尔值,甚至另一个数组。
我建议你把数组理解成一个“按顺序排好的货架”。货架有编号(索引),从 0 开始;货架上的每一格都有固定的位置,你可以往任意一格放东西、换东西、拿走东西。这样理解,后面所有操作都通了。
1.2 为什么单独把数组操作拎出来讲
对象操作往往比较简单——通过键名取值。数组不一样,它多了一层“顺序”的概念。顺序带来了三个新的问题:怎么定位某个元素(访问)、怎么把每个元素都过一遍(遍历)、怎么在保持顺序的前提下增删改。
还有一个容易忽略的细节:JSON 本身是一种数据交换格式,它不是可执行代码。所以当你把 JSON 文本解析成编程语言里的数组之后,操作方式完全取决于你的语言。比如 JSON 里的数组没有“方法”这个概念,但 JavaScript 的数组对象有push、splice这类方法;Python 的列表也有自己的一套 API。这就导致了一个常见困惑:有些操作在 JSON 层面根本不存在,必须等解析为语言内置结构之后才能做。
说到底,你需要掌握两件事:一是 JSON 语法层的数组规则(值用方括号、逗号分隔,不能有尾逗号),二是编程语言层的数组操作方法(增删改查)。这两件事搞清了,JSON 数组的操作你就掌握了 80%。
2. 访问数组值:索引、边界与常见误区
2.1 索引访问的正确姿势
访问数组值,默认方式就是通过索引。无论是 JavaScript 还是 Python,索引都是从 0 开始的。也就是说,数组第一个元素的索引是 0,第二个是 1,依此类推。
JavaScript 示例:
const fruits = ["苹果", "香蕉", "橙子"]; console.log(fruits[0]); // 苹果 console.log(fruits[1]); // 香蕉 console.log(fruits[2]); // 橙子Python 示例:
fruits = ["苹果", "香蕉", "橙子"] print(fruits[0]) # 苹果 print(fruits[1]) # 香蕉 print(fruits[2]) # 橙子这里有个小细节:Python 支持负数索引,fruits[-1]直接取最后一个元素,fruits[-2]取倒数第二个。这在处理尾部数据时非常方便。比如你想拿一个列表里的最后一条日志,Python 里一行代码搞定;JavaScript 里就得用fruits[fruits.length - 1],或者用 ES2022 新增的fruits.at(-1)方法。
但要注意,向前兼容性方面at方法在老旧环境里不可用,如果你还在维护需要兼容旧浏览器的项目,老实写arr[arr.length - 1]最稳。
2.2 越界访问:不会报错,但结果可能让你抓狂
访问数组时最容易踩的坑是索引越界。但这里有个反直觉的现象:JavaScript 和 Python 的规则不一样。
JavaScript 中,访问不存在的索引不会报错,而是返回undefined:
const arr = [1, 2, 3]; console.log(arr[5]); // undefined这样看似“宽容”,实则是隐患。如果你拿undefined去参与计算或拼接字符串,很可能得到一串莫名其妙的结果。比如某个字段取出来是undefined,你直接往页面上渲染,用户看到的就会是空或者是“undefined”这几个字。
Python 则严格得多,越界直接抛IndexError:
arr = [1, 2, 3] print(arr[5]) # IndexError: list index out of range所以 Python 写代码时,如果你不确定数组长度,最好先判断长度再访问:
if len(arr) > 5: print(arr[5])JavaScript 虽然没有报错,但也建议加个兜底判断:
const value = arr[5] ?? "默认值";这里??是空值合并运算符,只有当左边是null或undefined时才用右边的默认值。用它比用||更安全,因为||会把空字符串、0、false 这种“假值”也一起处理掉。
2.3 访问对象数组指定的属性值
数组里存对象时,访问方式就是“先取数组元素,再取对象属性”:
const users = [ {"id": 1, "name": "张三"}, {"id": 2, "name": "李四"} ]; console.log(users[0].name); // 张三 console.log(users[1]["name"]); // 李四两种写法都行,第二种适合属性名是动态拼接的场景。Python 里对应的是:
users = [ {"id": 1, "name": "张三"}, {"id": 2, "name": "李四"} ] print(users[0]["name"]) # 张三这里我强烈建议写代码时保持一致的风格,不要一会儿用点号一会儿用方括号,避免团队协作时阅读理解成本上升。
3. 遍历数组:各种姿势背后的性能与取舍
3.1 传统 for 循环:最基础也最可靠
遍历数组,最原始的方式就是 for 循环。它的好处是你同时掌握着索引,可以根据条件跳过、提前退出。
JavaScript:
const arr = ["A", "B", "C"]; for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }Python:
arr = ["A", "B", "C"] for i in range(len(arr)): print(arr[i])Python 里更推荐直接遍历元素:
for item in arr: print(item)但如果需要索引,可以用enumerate:
for i, item in enumerate(arr): print(i, item)在实际开发里,for 循环最大的价值是可以在中途break或continue。尤其是从数组里查找某个符合条件的数据时,一旦找到就退出,能省下不少无谓的循环。
3.2 JavaScript 的 for...of 与高阶方法
如果你不需要索引,那for...of是遍历数组最清爽的写法:
for (const item of arr) { console.log(item); }注意别用for...in遍历数组,那取到的是字符串类型的索引键,而不是数组元素,处理顺序也可能不按预期。
数组还自带很多遍历方法,比如forEach、map、filter、reduce。它们本质都是函数式遍历,但用途不一样:
forEach:只做遍历,不返回新数组。map:遍历并返回一个加工后的新数组。filter:遍历并筛选出符合条件的元素组成新数组。reduce:遍历并把数组归约为一个值。
这里我特别想提一个常见的性能误区:很多人喜欢在循环体里写arr.length,每轮循环都读取一次长度,性能上其实影响微乎其微。真正要警惕的是在循环过程中修改数组本身,比如边循环边splice删除元素,这会导致索引错位,漏掉元素或出现undefined。
3.3 遍历 JSON 对象数组:解构让代码更优雅
实际业务中,我们经常要遍历一个对象数组,然后只取其中几个字段。JavaScript 里配合解构赋值非常优雅:
const users = [ {"id": 1, "name": "张三", "age": 20}, {"id": 2, "name": "李四", "age": 30} ]; for (const {id, name} of users) { console.log(id, name); }Python 里如果字典结构固定,也可以类似解包:
for user in users: print(user.get("id"), user.get("name"))生产环境里,我建议对象数组里的字段都用get方法(Python)或可选链(JavaScript)来取,防止某条数据缺字段直接崩掉:
for (const user of users) { console.log(user?.name ?? "未知"); }4. JSON 对象中的嵌套数组:定位、遍历与展平
4.1 嵌套数组的取数逻辑
嵌套数组,就是在数组的元素里又遇到了数组,或者数组里包对象、对象里包数组、数组里再包数组。这种结构最典型的场景是“分类下挂多级子项”或者“多级菜单”。
看一个经典结构:
{ "categories": [ { "name": "电子产品", "items": [ {"id": "p1", "title": "手机"}, {"id": "p2", "title": "电脑"} ] }, { "name": "图书", "items": [ {"id": "b1", "title": "小说"} ] } ] }想取“电子产品”下面第一个商品标题,访问链是:
const data = JSON.parse(jsonText); const title = data.categories[0].items[0].title; // 手机这种一层层点下去的方式就是标准的“路径访问”。路径越长越容易出错,关键要分清楚每一层到底是数组还是对象,然后再决定用索引还是用键名。
一个实用技巧:如果你经常要访问深层数据,可以先做一步空值保护。JavaScript 里用可选链:
const title = data?.categories?.[0]?.items?.[0]?.title;这样即使中间某一层断了,也只会返回undefined,不会直接抛异常。Python 里没有可选链,但你可以用 try/except 兜底,或者用dict.get的方式逐层取:
categories = data.get("categories") or [] items = categories[0].get("items") if categories else [] title = items[0].get("title") if items else None4.2 多层嵌套数组的遍历:循环套循环
遍历嵌套数组,最直觉的方式就是嵌套循环。外层遍历第一层,内层遍历第二层。还是按上面那个分类的例子:
for (const category of data.categories) { console.log("分类名称:", category.name); for (const item of category.items) { console.log("商品标题:", item.title); } }Python 版本:
for category in data.get("categories", []): print("分类名称:", category.get("name")) for item in category.get("items", []): print("商品标题:", item.get("title"))这种循环嵌套写法本身没错,但三层以上代码就会变得很难维护。我建议一旦超过两层,就考虑用递归或者把结构先展平再用单层循环处理。
4.3 展平嵌套数组:从数据里“捞”出所有值
展平(flatten)是指把嵌套数组变成一维数组。比如[[1,2],[3,4]]变成[1,2,3,4]。
JavaScript 可以直接用flat方法:
const nested = [[1, 2], [3, 4]]; console.log(nested.flat()); // [1, 2, 3, 4]更深层的可以传参数:nested.flat(Infinity)表示无限层展平。
Python 里可以用列表推导式处理两层:
nested = [[1, 2], [3, 4]] flat = [x for sub in nested for x in sub] print(flat) # [1, 2, 3, 4]超过两层就别折腾推导式了,写个递归函数更清晰:
def flatten(arr): result = [] for item in arr: if isinstance(item, list): result.extend(flatten(item)) else: result.append(item) return result这个思路同样适用于处理任意深度的嵌套数组。在实际解析第三方接口时,嵌套数组展平后常常是为了方便做统计或筛选。
5. 修改数组值:直接赋值还是用方法,要分场景
5.1 通过索引直接修改
最简单的修改方式,就是拿到索引后直接覆盖。
JavaScript:
const arr = ["苹果", "香蕉", "橙子"]; arr[1] = "西瓜"; console.log(arr); // ["苹果", "西瓜", "橙子"]Python:
arr = ["苹果", "香蕉", "橙子"] arr[1] = "西瓜" print(arr) # ['苹果', '西瓜', '橙子']这里有个容易忽略的问题:如果数组原本长度是 2,你直接给arr[5]赋值,JavaScript 会把中间空出来的位置填上empty空位,打印出来是[元素1, 元素2, empty × 3, "新值"]。这种“稀疏数组”后续遍历时很可能产生非预期结果,所以最好避免。
Python 则是直接报错,不允许越界赋值。所以修改前先判断索引范围总没错。
5.2 批量修改:map 方法更符合“不变式”思维
在某些框架(尤其前端框架)里,直接改原数组有时不会触发视图更新。这时候用map返回新数组更合适。
比如把一组数字都乘以 2:
const arr = [1, 2, 3]; const doubled = arr.map(n => n * 2); // [2, 4, 6]map的好处是不会改动原数组,避免副作用。如果你需要在原数组基础上修改,也可以把结果重新赋值回去。Python 里等效的写法是列表推导式:
arr = [1, 2, 3] doubled = [n * 2 for n in arr] print(doubled) # [2, 4, 6]5.3 修改对象数组的子字段
当数组元素是对象时,我们常常只需要改里面某个字段:
const users = [ {"id": 1, "age": 10}, {"id": 2, "age": 20} ]; users[0].age = 15;这里有个性能小技巧:如果你要改整批对象的同一个字段,用循环或map都行。但要注意,用map时如果对象的引用没变,只是改了对象内部的属性,原数组对应的对象也会跟着变。这涉及 JavaScript 的引用传递,新手容易晕。要想彻底不可变,需要浅拷贝对象:
const updated = users.map(user => user.id === 1 ? {...user, age: 15} : user);这属于进阶话题,但实际开发里靠它解决了不少“改了没渲染”“上一个页面把数据串了”的诡异 bug。
6. 删除数组项目:三种方式,决策依据要清楚
6.1 splice 删除:精准且能指定数量
JavaScript 里真正用于从数组中删除元素的方法是splice。它的第一个参数是起始索引,第二个参数是删除几个元素:
const arr = ["a", "b", "c", "d"]; arr.splice(1, 2); console.log(arr); // ["a", "d"]arr.splice(1, 2)的意思是:从索引 1 开始,删除 2 个元素,也就是把 b 和 c 干掉,剩下 a 和 d。注意splice是直接修改原数组,同时返回被删除的元素数组。
如果删除后要立即插入新元素,splice也可以做到:
arr.splice(1, 2, "x", "y");这样就变成了先删除再替换。这个能力在你处理拖拽排序、列表动态增删时非常有用。
6.2 delete 操作符:不要用它删数组元素
JavaScript 里delete arr[1]会把索引 1 的元素删掉,但数组长度不会变,那个位置会变成一个空位(empty)。很多新人误以为这是“删除”,结果遍历时发现索引还在,只是值是空的,导致各种诡异问题。
所以对于数组,尽量不要用delete。它更适合用来删除对象的属性,而不是数组元素。
6.3 filter 过滤:按条件删除的本质
如果你想按条件删除元素,比如把所有小于 18 的项目删掉,那filter是最稳妥的:
const arr = [10, 20, 30, 15]; const filtered = arr.filter(n => n >= 18); console.log(filtered); // [20, 30]filter不会改变原数组,而是返回一个新数组。如果你确实想覆盖原数组,可以直接arr = arr.filter(...),但这要求变量不是用const声明。在某些框架里,这种“替换引用”的方式比直接改原数组更安全,更容易触发组件更新。
Python 里删除元素的方式分三种:
pop(index):按索引删除并返回该元素,类似splice的取回功能。remove(value):按值删除第一个匹配的元素。del arr[index]:按索引直接删除,不返回被删值。
arr = ["a", "b", "c", "d"] del arr[1] print(arr) # ['a', 'c', 'd']如果你要在循环里边遍历边删除,我强烈建议你换个思路:用“遍历索引倒着删”或者用生成一个新数组。正着删除会引起后面的元素前移,而循环变量还停在原来的步数,很容易跳过元素。
6.4 实际业务里的删除陷阱:数组是 const 时怎么办
很多人在做前端开发时,用const声明了一个数组,然后想通过filter重新赋值给这个变量,结果报错:
const list = [{id: 1}, {id: 2}]; list = list.filter(item => item.id !== 1); // TypeError: Assignment to constant variable.这种场景最常见的解法是换个做法,直接在一个新变量里保存结果,或者用splice方式原地修改:
const index = list.findIndex(item => item.id === 1); if (index !== -1) { list.splice(index, 1); }这个模式也经常用于表格数据删除。找到目标索引之后再删,比直接用filter重新设值更贴近“操作原数据”的语义。
7. 常见问题与排查技巧实录
7.1 索引越界与空数组
访问空数组的任意索引,JavaScript 返回undefined,Python 直接抛异常。如果你在解析一段不确定是否包含数据的 JSON,访问前先判断长度:
if (Array.isArray(data) && data.length > 0) { // 安全操作 }Python 里:
if isinstance(data, list) and len(data) > 0: # 安全操作很多线上报错都是因为后端返回了空数组,前端没有做保护就直接取下标 0,结果拿到一个undefined,再访问它的某个属性,直接报“Cannot read properties of undefined”。这可以说是 JSON 数组领域最经典的事故现场。
7.2 解析 JSON 文本时的类型陷阱
从接口拿到的原始数据是字符串,必须先解析成数组才能操作。JavaScript 用JSON.parse,Python 用json.loads。
解析时有一个容易忽略的问题:JSON 数组元素里并没有日期类型、undefined类型,只有字符串、数字、布尔值、null、对象、数组。所以如果你把一个 JavaScript 对象直接转成 JSON,再转回来,原来的undefined会消失,或者变成null。写序列化逻辑时要注意避免把关键字段搞丢。
另外,解析可能失败。后端偶尔会返回一段异常文本(比如错误提示而不是 JSON),直接JSON.parse会抛异常。稳妥做法是包一层 try/catch,或者在解析前先判断字符串是否以[或{开头,当然更合理的是看状态码判断是否正常返回。
7.3 遍历中删除导致索引错乱
这是一个频率极高的坑。假设你在 JavaScript 里这样写逻辑:找出所有值为某些元素的项并删掉,你通常会先 filter 再处理,或者用 while 循环加内部 splice。如果图省事在forEach里直接splice,你会发现漏删了某些元素,因为splice会让后续元素前移,而forEach的索引已经往后走了。
我给出的处理建议,按场景分成两种:
- 如果不关心原数组引用,直接
arr = arr.filter(condition)。 - 如果必须在原数组上删,就倒着遍历:
for (let i = arr.length - 1; i >= 0; i--) { if (shouldDelete(arr[i])) { arr.splice(i, 1); } }倒着删就不会影响前面位置的索引,所有splice的目标都是“当前尚未处理到的位置”,安全可靠。类似逻辑也适用于 Python。
7.4 嵌套数组取不到值的排查思路
当你的嵌套数组结构比较深,比如data.res.list[0].name,取不到值时,别急着往上补可选链。我一般按这个顺序排查:
- 先确认数据结构对不对:直接
console.log(JSON.stringify(data))打印出来看一眼。 - 确认是对象还是数组:用
Array.isArray(data.res)判断。 - 确认中间某层是不是空:比如
list[0]是否存在。 - 最后再考虑代码写法问题。
很多时候你以为的“嵌套数组取不到值”,其实是中间有一个字段名拼错了,或者后端返回了data.list,你写成了data.items。打印一遍原始 JSON,一步就定位了。
7.5 与后端联调时的数组字段一致性
前后端联调时,经常出现沟通问题:后端字段叫arr,前端代码里写data.array,结果界面永远空白。或者后端返回{"list": null},而不是[],代码里直接遍历null就炸了。
针对这种情况,我给前端同学一个建议:在和后端约定的接口文档里,明确数组字段的默认值。如果顶多为空,理应返回[]而不是null。但如果后端确实返回了null,你就在前端兜底:
const list = data.list ?? [];Python 后端也可以做类似防护:
data.get("list") or []如果项目里到处都得写这种兜底,说明后端接口设计不够规范,最好推动接口层修,而不是在前端各个角落打补丁。
8. 实操总结与经验延伸
写到这里,JSON 数组的访问、遍历、嵌套、修改、删除这些核心操作我都过了一遍。最后我再补充几个在实际项目里反复验证过的个人经验。
第一,处理 JSON 数组之前,先明确“你要操作的是原始 JSON 文本还是已被解析成语言结构的数组”。两者之间的转换是JSON.parse/JSON.stringify(JS)或json.loads/json.dumps(Python)。每次报错都该先想一想过层了没有。
第二,我自己的编码习惯是:能用filter和map解决的,尽量不用循环加 splice 的组合。这不仅让代码更短,还天然避免了“遍历时修改原数组”的坑。但要注意,filter和map生成新数组的开销对超大数组还是有影响的,如果你在跑几十万条数据的批处理,传统循环可能更稳。
第三,嵌套数组千万别硬背路径,写代码时多用可选链或者 Python 的 dict get 兜底。之前我在某个项目里看到一个接口返回的层级深到 5 层,大家每层都去判断非空,代码写了一大坨。后来把数据结构梳理清楚,用一个固定的取值函数统一处理,代码清爽了不少。核心思路是:把嵌套数组的访问逻辑收敛到一处,不要散落在各处。
第四,也是我最想强调的一点:JSON 数组是数据交换的“通用语言”,但不同的编程语言处理它的细节差异很大。你完全不需要背每种语言的 API,只需要记住几个核心概念——索引从 0 开始、数组长度、越界行为、原地修改 vs 返回新数组。这四个概念拿出来,不管换到什么语言,都能快速适应。
最后分享一个小技巧:调试 JSON 数组时,不要光看console.log(arr),因为有些浏览器控制台会惯性展开实时数据,反而看不清当时的快照。我习惯用console.log(JSON.stringify(arr))打印出字符串形式,所有结构一眼看穿,而且是当时时刻的真正状态。这招在排查复杂嵌套数组时帮我省了不少时间。