
写这篇东西的起因是最近在团队 Code Review 时又看到一个把sizeof用在数组函数参数里的老问题编译没报错运行结果却和预期差出十万八千里。数组作为编程里最基础的数据结构几乎每种语言都有但恰恰因为“太基础”很多人对它的细节一知半解踩坑踩到怀疑人生。我自己这些年调试过的数组相关 Bug从 C 语言的越界写坏内存到 Python 切片的浅拷贝陷阱再到 JavaScript 数组方法修改原对象的隐性联动零零总总加起来能写一本小册子。所以今天干脆把“数组易错点”这个主题系统整理一遍把那些容易混淆、容易犯错、容易忽略的点全部摊开讲希望能帮你在下次写代码时少掉几次头发。1. 数组初始化与赋值的几个经典坑1.1 大小指定与初始化列表的长度关系很多新手第一次写 C 语言数组时会觉得“数组大小不写也行编译器会自动算”。这句话对了一半要看上下文。int a[] {1, 2, 3}; // 合法数组大小为3 int b[3] {1, 2, 3, 4}; // 错误初始化列表元素多于数组容量 int c[5] {1, 2, 3}; // 合法剩余元素被零填充第三行是很多人忽略的“默认零初始化”。c[5]只有前三个赋了 1,2,3后面的c[3]和c[4]会被自动置为 0。这在嵌入式开发里经常被依赖但也经常出问题你以为只初始化了前几个实际后面全是 0如果把这当“未初始化”去读可能错过真正的数据。反过来如果数组声明时不指定大小比如int d[] {1,2,3,4,5};那就完全依赖初始化列表的长度后续想扩充元素就只能改初始化列表代码可维护性很差。我个人的建议是显式指定数组大小除非是映射到外部固定长度数据的场景。显式写清楚长度编译期就能帮你揪出一部分溢出问题也能让代码阅读者一目了然地知道这块内存的边界。1.2 字符数组与字符串根本不是一回事C 语言里没有原生字符串类型字符串本质上是字符数组但多了一个规矩以\0结尾。这个“多出来的一个字节”坑了不少人。char str1[3] {a, b, c}; // 不是字符串没有结尾符 char str2[4] abc; // 合法自动加 \0 char str3[3] abc; // 编译警告/错误没有空间放 \0用strlen去量str1它会一直往后读直到碰到内存里的某个 0结果完全不可预知。这就是典型的“缓冲区未终止”漏洞的根源。很多安全漏洞比如 HTTP 解析、文件解析就是从这里来的。另一个常见错误是给字符串指针分配空间时忘掉结尾符char *p malloc(strlen(s)); // 错误应该 malloc(strlen(s) 1) strcpy(p, s);strlen返回的是不含\0的长度但strcpy会把\0也拷过去。少分配一个字节就越界写了一个字节。这种错误在堆上出现不会立刻崩溃往往等到内存被破坏得很严重时才暴露调起来非常痛苦。记住一个习惯凡是跟 C 字符串打交道分配空间时在大脑里都要自动1。1.3 静态数组、局部数组的默认初始化差异C/C 中数组初始化规则有一个很容易忽略的“默认值”区别全局数组或静态数组默认初始化为 0。局部数组栈上数组默认初始化为“不确定值”也就是上一块内存留下的垃圾数据。所以如果你写void func() { int arr[10]; // 直接读 arr[3]可能得到一个随机值 }这在 C 语言里是未定义行为但在某些编译器下只是读到垃圾值。如果你拿这个垃圾值去做条件判断程序行为就会变得飘忽不定。C 里局部数组默认也是不初始化的但如果你用了std::arrayint, 10 a;它同样不会默认清零需要a.fill(0)或者用值初始化语法std::arrayint, 10 a{};才能清零。这一点在嵌入式平台和底层开发中尤其关键我见过太多因为忘记初始化数组导致偶发故障的案例最后定位到都是局部数组垃圾值参与运算。2. 数组与指针最容易懵的一对2.1 数组名不是指针但会“退化”成指针教科书上经常说“数组名就是指向首元素的指针”这句话在 99% 的场景下能用但在sizeof和操作符面前就失效了。int arr[10]; printf(%zu\n, sizeof(arr)); // 40假设int为4字节 printf(%zu\n, sizeof(arr[0])); // 8指针大小64位系统sizeof(arr)得到的是整个数组占用的字节数而sizeof(指针)只是指针本身的大小。这个区别是无数 Bug 的源头。当数组作为函数参数传递时它不会把整个数组拷贝进去而是“退化”为指向首元素的指针。这就是为什么在函数内部使用sizeof得到的是指针大小而不是数组大小。理解“退化”的准确场景很重要作为函数参数时int arr[]和int *arr完全等价。作为右值赋值给指针时数组名隐式转换为首元素地址。但arr得到的是指向整个数组的指针类型是int (*)[10]和arr[0]类型不同。很多人会在arr和arr[0]上翻车它们打印出来的地址数值可能一样但指针算术不一样arr 1会跳过整个数组 10 个元素而arr[0] 1只跳过一个元素。这点在写底层内存遍历代码时经常造成潜伏很深的 Bug。2.2 指针数组与数组指针高频混淆点中文术语本身就让人头大但英文反而更好记指针数组char *arr[5]本质是“数组”每个元素是一个指针。数组指针char (*arr)[5]本质是“指针”它指向一个有 5 个元素的数组。区别就在括号上。*和[]的优先级不同[]的优先级高于*所以不加括号时char *arr[5]先被解释为“数组”元素类型是char *。加了括号char (*arr)[5]*先和变量名结合所以它是个指针指向一个长度为 5 的 char 数组。指针数组在实际中使用很广比如存放多个字符串char *fruits[] {apple, banana, cherry};这里每个元素都是指向字符串字面量的指针注意字符串本身是只读的如果你尝试修改fruits[0][0]在很多平台会直接崩溃。这是另一个易错点用指针声明的字符串数组不能通过下标修改字符但用二维字符数组char fruits[][10]则可以因为后者为每个字符串分配了可写的连续空间。2.3 二维数组与指针的复杂关系二维数组的“退化”规则让很多人抓狂int matrix[3][4]; int (*p)[4] matrix; // 正确matrix退化为指向“含4个int数组”的指针如果把matrix赋给int **通常编译器会报错或行为错误。原因很简单int **指向一个指针变量而二维数组内存是连续排列的并没有额外存储指向每行的指针。所以用int **去操作二维数组等于拿一套错误的内存布局去寻址轻则读取错误重则内存越界崩溃。正确访问二维数组元素有两种方式下标法matrix[i][j]最直观。指针法*(*(p i) j)或p[i][j]后者可读性更好。另外要注意二维数组作为函数参数时第二维的大小必须明确写出void func(int arr[][4], int rows); void func2(int (*arr)[4], int rows); // 这两种写法等价省略第二维会导致编译器无法计算行长度这是数组退化规则直接导致的。C 里可以用std::arraystd::arrayint, 4, 3或std::vectorstd::vectorint但那种“每行长度相等”的二维语义又与传统的连续内存布局不同选择时要根据性能需求和代码可读性权衡。3. 数组作为函数参数时的退化与长度丢失3.1 为什么 sizeof 在函数里变了样这个问题太经典了我几乎每次面试都会问写一个函数接收数组参数打印数组长度为什么sizeof(arr) / sizeof(arr[0])在函数里总等于 1void print_length(int arr[]) { printf(%zu\n, sizeof(arr) / sizeof(arr[0])); // 永远是1或2取决于指针大小/int大小 }原因已经在前面提过函数参数里的int arr[]会被调整成int *arrsizeof(arr)是指针大小在 64 位系统上是 8sizeof(arr[0])是 4除出来是 2如果在 32 位系统上两者都是 4除出来是 1。不管哪种都不是你想要的实际数组长度。要正确处理必须要额外传一个长度参数void print_length(int *arr, int len) { for (int i 0; i len; i) { // ... } }最好的做法是调用方负责传sizeof(arr) / sizeof(arr[0])但要注意只有在“数组真正是数组”的上下文里才能这样做。如果变量本来就声明为int *p那么sizeof(p)/sizeof(p[0])没有任何意义。这个规则就是“数组只有在未被退化时才能用 sizeof 求长度”。3.2 字符串数组作为参数的隐藏要求字符串数组可以用char *strs[]或者char strs[N][M]来定义。作为参数时两种写法差别很大void func1(char *strs[], int count); // 指针数组每个元素指向一个字符串 void func2(char strs[][MAX_LEN], int count); // 二维数组每个字符串固定最大长度如果你在函数内部要遍历这些字符串func1需要知道每个字符串的终止位置靠\0而func2里每个字符串占用的空间是固定的即使字符串更短剩余空间也是垃圾或填充。直接用strlen在func2里依然可以工作但写回操作时要特别小心不能超过MAX_LEN-1否则会把下一行的数据覆盖掉。在 C 里常见的做法是改用std::vectorstd::string但如果你是维护老代码必须面对 C 风格的字符串数组建议在约定里写清楚“数组长度”“每行最大长度”“是否以空字符结尾”三个约束否则调用方传错一个参数函数里就会出现隐蔽的越界写。3.3 CString 转 char 数组MFC 老了但坑还在很多老项目还在用 CString转成 char 数组时也有经典坑CString cstr hello; char buf[32]; strcpy(buf, (LPCTSTR)cstr); // 在 Unicode 编译环境下LPCTSTR 是 wchar_t*类型不匹配正确做法通常是CStringA cstrA cstr; // 或使用 CT2A 转换宏 strcpy(buf, cstrA.GetString());这里最大的坑是多字节与 Unicode 之间转换时缓冲区长度要按转换后的字符数计算如果源字符串包含中文直接把wchar_t*强转成char*会得到乱码和数据丢失。写strcpy时也别忘了给\0留位置。我建议现代代码尽量用std::string或std::wstring彻底绕开 CString 转换的缓冲区管理和编码问题。4. 动态数组与内存管理的责任划分4.1 C 语言动态数组的分配与释放动态数组能让运行时决定大小但代价是必须手动管理内存。最常见的错误包括忘记释放分配的内存导致内存泄漏。释放后继续使用该指针成为悬空指针。重复释放同一个指针导致崩溃或未定义行为。在分配失败后未检查指针是否为 NULL直接使用导致崩溃。一个相对完整的分配与释放模式int *arr NULL; size_t n 10; arr (int *)malloc(n * sizeof(int)); if (arr NULL) { // 处理分配失败 } // 使用数组... free(arr); arr NULL; // 习惯性置空防止悬空指针难点在于“所有权”问题如果函数内部分配了数组又把指针返回给调用方那么谁负责释放约定不清晰很容易一会在函数里释放一会在外面释放。我习惯的做法是谁分配谁负责释放如果返回给调用方要在文档或注释中明确说明“返回值需要调用者 free”。如果代码逻辑复杂可以考虑设计一个小的数据结构封装数组和长度统一管理而不是裸指针满天飞。4.2 C 中 new[] 与 vector 的选型C 里动态数组可以用new[]或std::vector。很多人觉得vector用起来更高级但在性能敏感的场景下new[]更轻量。问题是new[]的坑也不少int *arr new int[10]; delete arr; // 错误应该用 delete[] delete[] arr; // 正确new/delete和new[]/delete[]必须成对匹配。如果不匹配某些平台上会直接崩溃因为分配器可能记录了不同的大小信息。为了避免这类问题建议优先使用std::vector它不用你操心释放、扩容和迭代器失效问题。如果一定要用new[]尽量用std::unique_ptrint[]或std::shared_ptrint[]来包裹这样既保留原始数组的访问方式又能自动释放。4.3 数组扩充的常见实现方式动态数组需要一个常见操作扩容。C 语言里通常用reallocint *arr malloc(4 * sizeof(int)); int *new_arr realloc(arr, 8 * sizeof(int)); if (new_arr ! NULL) { arr new_arr; }realloc有两个值得注意的点它可能原地扩容也可能搬移到新地址。如果失败返回 NULL但原来的内存还保留着。如果直接写arr realloc(arr, new_size)一旦失败arr被置成 NULL原先的内存泄露且无法释放。所以必须先赋值给临时指针。扩容后新增加的元素的值是未初始化的需要你手动赋值。C 中如果你自己管理数组扩容步骤是分配新大小、拷贝/移动旧元素、释放旧内存。但std::vector已经内置了这个逻辑而且它还有“容量增长因子”的优化通常按 1.5 倍或 2 倍增长减少反复扩容的开销。所以日常开发里除了极底层场景真的不用手写扩容。5. 数组遍历与越界最隐蔽的 Bug 来源5.1 下标越界的后果与边界陷阱数组访问越界在 C/C 里不会自动报错行为完全看运气。越界读取通常只是拿到垃圾值越界写则可能破坏相邻变量、破坏返回地址、破坏堆管理结构最恶劣的情况是程序运行一段时间后才崩溃极难定位。边界陷阱的典型例子for (int i 0; i N; i) { // 应该是 i N arr[i] 0; }多等号写成了当i N时就越界写了一个元素。这种错误在编译时完全不报错只能通过代码审查或工具检测如 AddressSanitizer、Valgrind揪出来。我个人经验是写循环时默认使用半开区间[0, N)也就是i N不要用闭区间。这个习惯可以消灭一大半越界错误。另一个容易忽略的边界问题是数组下标从 0 开始还是从 1 开始。在数学公式实现时很多算法描述用 1-based但代码里是 0-based转换时容易漏掉 ±1。我见过一个矩阵乘法实现里错了一个下标位移导致结果矩阵整体偏移一行调试了三天。建议在实现算法前先把坐标系转换关系写清楚再动手写循环。5.2 遍历时修改数组的常见错误遍历数组时如果同时删除或插入元素索引管理就成了大坑。以 JavaScript 为例const arr [1, 2, 3, 4, 5]; for (let i 0; i arr.length; i) { if (arr[i] % 2 0) arr.splice(i, 1); }这段代码的本意是删除所有偶数但实际执行结果不会删干净因为splice会让后续元素往前移动i却继续递增导致跳过下一个元素。解决办法是倒序遍历for (let i arr.length - 1; i 0; i--) { if (arr[i] % 2 0) arr.splice(i, 1); }或者用filter生成新数组避免修改原数组。这个坑在 Java 的ArrayList里也一样用迭代器删除时不能调用list.remove必须用iterator.remove否则会抛ConcurrentModificationException。我的经验是能不原地修改集合/数组就尽量不原地修改新数组和不可变风格能规避一大批诡异 Bug。5.3 Python 数组切片、NumPy 数组的注意点Python 的列表切片arr[start:end]返回的是新列表但如果是多维 NumPy 数组切片返回的是视图View不是拷贝。修改视图会直接改原数组import numpy as np a np.array([[1, 2], [3, 4]]) b a[:, 0] # 视图 b[0] 99 print(a) # [[99, 2], [3, 4]] 原数组也被改了如果不想影响原数组就显式用.copy()b a[:, 0].copy()NumPy 三维数组相乘也是新手常翻车的地方。二维矩阵相乘用np.dot或但三维以上就要特别小心维度对齐。比如np.multiply是逐元素乘才是矩阵乘。两个三维数组np.matmul时要求最后两维满足矩阵乘条件前面维度要 broadcast。很多人把*当成矩阵乘结果得到逐元素积在神经网络实现里数据完全错误。建议操作前先打印.shape确认维度不要靠想象。6. 数组排序、去重与算法题的坑6.1 排序时改变原数组的副作用排序是数组操作里最常用也最容易忽略“副作用”的场景。JavaScript 的arr.sort()默认是原地排序而且比较规则比你想的更诡异——元素默认按字符串的 Unicode 码点排序[10, 9, 100].sort(); // 结果是 [10, 100, 9]因为 10 和 100 都被转成字符串10、100首位1相同再看第二位0与0然后 10 比 100 短所以 10 排在 100 前面。正确做法是传比较函数[10, 9, 100].sort((a, b) a - b); // [9, 10, 100]另外sort修改的是原数组如果业务逻辑里还需要保留原顺序必须先用[...arr]或arr.slice()做一份拷贝。Python 里list.sort()也是原地排序但内置函数sorted()会返回新列表。Java 里的Collections.sort也是原地排序如果不想动原集合就新建一个 List 再排序。这个“原数组是否被改”的问题在团队协作中经常因为隐式约定不同而互相踩最好在函数命名上就区分清楚sort表示原地sorted表示返回副本。6.2 数组去重的多种实现与性能对比数组去重是面试高频题三种常见写法各有各的坑用Set最简单但请注意Set去重时使用 SameValueZero 规则NaN和NaN会被认为是相同的{}和{}对象则不会合并因为它们引用不同。filter indexOf时间复杂度 O(n^2)大数据量下很慢而且indexOf对NaN的处理会失效。reduce构建对象/Map可以自定义去重逻辑比如对象去重时只要某个属性相同就认为重复这才是实际项目中更常见的需求。Java 里LinkedHashSet可以保持插入顺序但如果元素是自定义对象必须重写equals和hashCode。很多人只重写equals不重写hashCode导致HashSet认为所有 new 出来的对象都不相等。C 里用std::unordered_set去重自定义类型时要提供哈希函数和相等比较器这两个必须保证一致否则同一个逻辑对象可能哈希不同去重失败。对象数组去重这种需求在实际项目中非常常见我的建议是不要偷懒用字符串化因为JSON.stringify之后对象的键顺序可能不一致导致“明明相同却被认为不同”。最好指定一个唯一的业务键例如clientId构建一个 Map然后过滤。6.3 树状数组与区间最值算法题中的易错点数组的高级用法里树状数组Fenwick Tree和线段树是处理区间查询、区间更新的利器但实现时也有几个顽固易错点。树状数组模板中核心是lowbit操作int lowbit(int x) { return x (-x); }更新和查询都依赖这个函数。常见错误如下数组下标从 1 开始还是从 0 开始没有统一。树状数组通常约定下标 1..n但很多题目的输入是 0-based如果不做偏移update的第一个位置会永远更新不到。update循环的终止条件写错for (int i x; i n; i lowbit(i))这里的n是最大下标不是数组长度减一。query部分求和时i - lowbit(i)写成了i - lowbit(x)或者循环写成了i 0与i 0的区别导致额外访问tree[0]而这个位置通常没初始化。求区间最大值时树状数组也可以实现“前缀最大值”但如果是任意区间最大值建议还是上线段树。用树状数组硬写区间最大值更新和查询逻辑会出现特例很容易错。如果追求代码简单ST 表Sparse Table适合不可变数组的区间最值查询但要注意预处理时区间长度倍增的边界以及查询时两个区间重叠对应的k取值。一个容易错的点是len r - l 1, k log2[len]然后取max(st[l][k], st[r - (1 k) 1][k])右边起始位置写错会导致覆盖区间不完整。7. 跨语言数组差异化避坑指南7.1 数组与集合/列表的混淆JavaScript 里所有数组都是动态的它的“数组”更像 ArrayList。C# 里数组是固定长度的ListT才可变。Kotlin 里IntArray是原始类型数组ArrayInt是装箱数组后者有额外内存开销。这种“同名不同义”很容易让多语言开发者切换时掉坑。Java 数组是协变的String[]是Object[]的子类型所以可以把String[]赋给Object[]这导致了著名的“数组存储检查”问题Object[] arr new String[10]; arr[0] 123; // 编译通过运行时报 ArrayStoreException这种设计是历史遗留。日常开发中Java 里更推荐用ArrayList而不是数组但如果必须用数组就要把“协变但运行时要检查”的特性记在心里。JavaScript 中有“稀疏数组”概念const arr []; arr[100] 5;这个数组的长度是 101但中间 99 个元素是空位。forEach不会遍历空位而map也会跳过空位但保留它们这可能导致统计结果和直觉不符。如果你要严谨处理可以用Array.from或Array(n).fill(undefined)把空位填充成undefined。7.2 JavaScript 数组方法的典型陷阱删除指定元素、合并去重、转字符串array.splice(索引, 1)是删除指定位置元素但很多人会因为不知道splice会修改原数组而困惑。删除指定值更稳妥的方法是先indexOf找到索引再splice如果有多个重复值就要循环删。但前面已经提到循环删除时一定要倒序遍历。数组合并去重是另一个常见场景const a [1, 2, 3]; const b [2, 3, 4]; const merged [...new Set([...a, ...b])];这里的坑是Set能把NaN当成同一个值但[...a, ...b]如果是对象数组它是引用去重不会按内容去重。如果要按对象某个属性去重你需要手动写Map过滤或者用lodash的uniqBy。数组转字符串也有两种容易混的思路arr.toString()和arr.join(,)结果类似但toString对 null/undefined 的处理是空字符串join也一样。JSON.stringify(arr)会把数组转成 JSON 字符串保留null和数组结构适合传输和深拷贝但普通使用时不方便。如果想把字符串转回数组split(,)很简单但如果是 CSV 里含逗号的字段就得考虑正经的 CSV 解析库不能直接split。7.3 Python、NumPy、Excel/VBA 数组操作中的特有问题Python 内置列表list和 NumPy 数组的索引语义差异很大。list[1:]会创建新列表但 NumPy 切片是视图前面已经提过。再比如list的会原地扩展还是生成新对象跟numpy的也有微妙区别。VBA 数组对很多人来说像上古遗留技能但 Excel 自动化里离不开它。VBA 里的数组有两种固定长度和动态ReDim。动态数组使用前必须先ReDim确定长度而且ReDim会清空数组内容需要用ReDim Preserve保留原有数据。这个Preserve是耗性能的关键如果循环里反复ReDim Preserve扩容Excel 会卡到怀疑人生。更合理的方式是估一个比较充足的上限一次ReDim到位最后再裁剪长度或者直接使用 ArrayList。另一个 Excel 相关的高频需求是“提取前两列匹配的数据组成数组”。通常的做法是循环遍历 Range用WorksheetFunction.Match或字典对象做映射。这里的坑是 Excel 返回的数组如果是二维数组行列维度和 VBA 数组的下标默认从 1 开始跟你声明的Dim arr(0 To 9)下标从 0 开始混淆。建议在做任何数组遍历前先UBound和LBound确认边界不要硬编码 0 作为起始下标。7.4 C 多维数组、一维数组练习题中的惯性思维C 里vectorvectorint虽然用起来方便但它的内存不保证连续如果要传给一些要求连续内存的 C 接口就不能直接传v.data()。反过来C 风格二维数组的连续内存布局适合用memset快速清零但vectorvector则不能memset否则会破坏内部指针。一维数组似乎最简单但练习题中经常出现“交换数组元素”“循环移位”“找最大最小”等操作最容易错的是交换时的临时变量处理以及移位时边界条件。比如循环右移 k 位有人直接for (int i 0; i k; i) { 整体右移一位 }复杂度 O(n*k)数据量大时超时。正确做法是三次反转序列reverse(arr, 0, n-1); reverse(arr, 0, k%n-1); reverse(arr, k%n, n-1);一次搞定复杂度 O(n)。这个技巧我在面试中见过很多人想不到但它其实揭示了数组操作的底层规律翻转操作多么常用。8. 数组在实际工程中的几个经验建议8.1 建立边界意识二分、双指针与区间最值算法题里二分查找是对数组下标的极限考验稍不留神就会死循环。经典错误是int l 0, r n - 1; while (l r) { int mid (l r) / 2; if (nums[mid] target) l mid; // 错误应该 l mid 1否则可能死循环 else if (nums[mid] target) r mid; // 同理应为 r mid - 1 }一旦某个区间长度为 2mid取左端点l mid会让区间不再缩小死循环。正确的二分判断必须保证每次循环范围严格缩小要么l mid 1要么r mid - 1或者使用“左闭右开”写法l r配合r mid/l mid 1。这里没有捷径只能靠大量练习形成肌肉记忆。双指针问题快慢指针、左右指针的易错点在于指针移动的时机和结束条件。比如移除数组中的重复元素用快指针遍历慢指针维护结果数组结束条件写错会导致慢指针落后一拍或越界。建议在代码中写清楚“指针是指向下一个可写位置”还是“指向最后一个有效位置”这样就不会在判断条件里犯迷糊。8.2 避免“数组灾难”的编码规范从工程角度看裸数组最大的问题是没有长度信息。你在函数调用之间传递一个int *根本不知道它指向多少个元素。改良方案是C 里用std::span或std::vector作为函数参数。C 语言里把数组和长度封装成一个结构体。Java 里尽量用List如果必须用数组至少提供length字段和访问方法。JavaScript/TypeScript 里可以用只读数组ReadonlyArrayT防止误修改原数组。另外现代编译器都有丰富的警告选项请务必开启-Wall -WextraClang/GCC 还有-fsanitizeaddress能帮你捕获很多越界和未定义行为。把这些检查加进 CI 流程比起靠人眼发现数组 bug效率高得多。8.3 数组与函数式编程用新数组取代原数组修改函数式编程风潮下map、filter、reduce等操作让数组处理更安全因为它们通常不修改原数组。实践经验是如果你发现自己正在用循环原地操作数组并且需要维护好几个状态变量比如当前索引、计数器、是否拼接那大概率可以用一个reduce把事情表达得更清晰。但也要注意reduce的累加器如果是对数组的“拼接”操作性能可能比循环差因为每次都会生成新数组。这时用for...of循环配合push反而是更优解。我的个人经验先写可读性高的代码再用性能分析工具来优化不要在一开始就进行“手动原地修改数组”之类的花哨操作。代码是要给人读的人读不懂的地方就是 bug 滋生的地方。写到这里最后再分享一个我自己的习惯任何涉及数组的代码先写边界测试再写实现。比如写一个反转数组的函数先测空数组、长度为 1、长度为 2、奇偶长度各一遍。数组的 bug 绝大多数集中在边界边界测试能消灭掉九成问题。如果你每次写完数组相关逻辑都顺手跑几个边界用例你会发现自己省下的调试时间远超写测试的时间。这是一个投入产出比极高的习惯也希望读到这里的你能真的用起来。