news 2026/9/9 17:02:01

手写JavaScript数组三大方法:forEach、filter、every的底层实现与陷阱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写JavaScript数组三大方法:forEach、filter、every的底层实现与陷阱

有阵子没写这种“手写实现”系列了。今天聊三个看起来特别简单、但细挖起来全是细节的数组方法:forEachfilterevery。大多数人在项目里天天用,但真要让你当场手写一个,或者解释为什么forEach不能用return跳出循环、filter会不会改写原始数组、every对空数组会返回什么,很多人会卡壳。这篇文章就把这三个方法的底层行为、手写实现和隐藏坑全翻出来,不管你是准备面试,还是想加深对JavaScript数组的理解,都能找到想要的东西。

1. 内容整体设计与思路拆解

1.1 这三个方法到底有多高频?——从业务场景看需求

在日常业务代码里,数组操作几乎绕不开forEachfilterevery。比如拿到后端返回的订单列表,要遍历渲染成表格,最顺手的就是forEach;要按条件筛选出已支付的订单,filter一行搞定;要判断一组表单是否全部通过校验,every是最直观的答案。它们对应着三种最基本的集合运算:遍历、筛选、全真判断。

你去看现在的前端代码,只要业务稍微复杂一点,这三个方法几乎会同时出现。举个例子,一个购物车页面,需要计算选中商品的总价,你可能会先用filterisSelectedtrue的商品筛出来,再用forEach累加价格;如果还要判断是否所有商品都有库存,every又派上用场。可以说,不掌握这三个方法,写数组逻辑会处处碰壁。

而且,这三个方法不仅仅是工具函数,它们背后蕴含着函数式编程的核心思想:不直接写循环、不手动管理索引、用回调描述“做什么”而不是“怎么做”。这也是为什么很多人前端写了几年,一改用原生循环就浑身不自在,因为高阶函数带来的抽象能力确实让代码更清晰、更不容易写错。

1.2 为什么要学习底层实现?——面试、踩坑和函数式编程

很多人觉得这些方法浏览器已经实现了,直接调不就行了?但正因为它们太底层,我们才要理解它的内部逻辑。

一方面,大厂面试常考手写这些方法,考察你对回调函数、this指向、循环边界的掌握。我曾经面试过一个候选人,他能很流利地背出这三个方法的用法,但让他手写一个filter,他却直接写出了for循环加push,却忘了处理稀疏数组,也忘了回调中thisArg的绑定。这不是背不背得出来的问题,而是对语言规范的理解深度不够。

另一方面,真实项目中经常出现隐性bug,根源就是对实现机制不清楚。最常见的例子:在forEach里用return试图终止循环,结果发现后面的元素照样被处理;或者用filter筛选却没有接收返回值,导致筛选结果丢失;再或者对一个空数组调用every,得到true,让一些不熟悉规范的人大吃一惊。这些坑如果你读过它们的实现,基本不会踩。

熟悉底层实现还有一个实际好处:你可以在无法使用ES5+的环境里,自己写一个polyfill兼容老浏览器。虽然现在ES5以上环境已经非常普及,但在某些特殊的运行环境——比如老版本的安卓WebView、某些嵌入式JavaScript引擎中,原生方法可能缺失或行为不一致。这时候,手写实现就是救命稻草。

1.3 一个共同的技术底座:回调函数如何驱动

这三个方法本质都是遍历数组元素,依次调用你传入的回调函数。区别在于:

  • forEach只负责遍历,回调的返回值被丢弃;
  • filter根据回调返回的布尔值决定是否保留元素,收集到一个新数组;
  • every则是只要回调返回一个假值,立刻终止遍历并返回false

如果你掌握了这个共同底座,手写起来就会非常顺畅。比如,forEachfilter的循环骨架几乎一样,唯一区别是filter多了一个result.push(value)的条件分支;every则多了一个提前return false的出口。所以,千万别把这三个方法当成独立的API去死记硬背,把它们看成一棵树上的三个果实,会容易得多。

另外,它们还有一些共同的细节需要留意:回调中会收到三个参数(当前值, 当前索引, 原数组);第二个可选参数thisArg用于指定回调中的this指向;遍历过程中如果数组被修改,行为可能会很微妙。这些细节在后面的实现代码里会一一展开。

2. 核心细节解析:forEach的完整实现

2.1 forEach的API规范和行为特征

forEach的官方定义是:对数组的每个元素执行一次给定的函数。它的API签名是:

arr.forEach(callback(currentValue [, index [, array]]) [, thisArg]);

这里有个关键点:forEach没有返回值,或者说返回undefined。很多人误以为它返回一个新数组,实际上它只做遍历。如果你想拿到处理后的结果,应该用map,或者在外部变量中累积结果。

另一个关键行为是:forEach不会遍历数组中的空位(稀疏数组)。什么叫空位?比如const arr = [1, , 3];,中间那个位置没有值,访问arr[1]会得到undefined,但它其实是一个“空位”,而不是值为undefined的元素。forEach会跳过这个空位,不调用回调。这一细节在做性能优化或兼容时特别重要。

还需要注意的是:forEach没有跳过早退机制,不像for循环可以用breakreturn中断。如果需要在满足条件时停止遍历,forEach不是合适的选择,可以考虑for...ofsome/every(它们在某些场景下会提前退出)。

2.2 手写forEach:从零开始,逐行拆解

直接给出一个符合ES5规范的手写实现,然后逐行解释:

Array.prototype.myForEach = function(callback, thisArg) { // 1. 检查this是否为null或undefined if (this == null) { throw new TypeError('Array.prototype.myForEach called on null or undefined'); } // 2. 检查callback是否为函数 if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } // 3. 将this对象化,并取出数组长度 const arr = Object(this); const len = arr.length >>> 0; // 4. 从索引0开始遍历 let k = 0; while (k < len) { // 5. 只处理真实存在的元素,跳过稀疏数组的空位 if (k in arr) { // 6. 调用回调,指定thisArg,并传入三个参数 callback.call(thisArg, arr[k], k, arr); } k++; } };

让我逐条解释为什么每个步骤都不能少。

第一步为什么用this == null而不是!this?因为this可能被显式设置为nullundefined,也可能是0、空字符串等假值。规范要求只有nullundefined会被拒绝,如果传入0,应该正常执行(当然,0上没有length属性,实际不会有元素)。用==是为了同时判断nullundefined,这里用宽松相等是刻意为之,严格相等=== null则无法覆盖undefined

第三步Object(this)是为了兼容基本类型。如果调用[].myForEach.call('abc', fn),字符串会被包装成String对象,拥有length和索引属性,所以forEach也能遍历字符串。len = arr.length >>> 0是一种将任意值转换成非负整数的标准技巧。>>> 0会把负数变成很大的正数,把小数截成整数,这看起来有点反直觉,但能确保后续循环不会越界。

第五步if (k in arr)就是处理稀疏数组的关键。in运算符会检查属性是否存在(包括原型链),对于空位,属性不存在,所以跳过。对于值为undefined的元素(比如const arr = [undefined, undefined]),空位与显式undefined的区别就能体现出来:显式undefined元素会正常执行回调。

第六步callback.call(thisArg, arr[k], k, arr),如果调用时没有传入thisArg,在非严格模式下thisArg会是undefined,而回调中的this会被自动绑定到全局对象;在严格模式下则保持undefined。这个行为与原生的forEach一致。如果你想避免这种混淆,可以在调用时强制thisArg = thisArg,但规范实际上就是这么处理的。

这里有个小技巧:如果你不想修改原生原型,其实不用挂在Array.prototype上,可以直接写一个普通函数:

function myForEach(arr, callback, thisArg) { ... }

但挂在原型上,更贴近原生API的调用方式,也方便在面试中演示。注意,在生产环境里,尽量不要去修改内置对象的原型,避免污染全局。

2.3 forEach的陷阱:稀疏数组、break失效、修改原数组

先说稀疏数组。我们用上面的myForEach测试一下:

const arr = [1, , 3]; let sum = 0; arr.myForEach((v) => { sum += v; }); console.log(sum); // 4

空位被跳过,所以只有1和3参与累加。如果你用for循环直接遍历,那么arr[1]undefined,就会变成sum += undefined,得到NaN。所以当你需要保持“跳过空位”的语义时,直接用原生forEach或手写实现会更安全。

再说break失效。很多人误以为回调里的return能终止forEach,其实不能。这是因为forEach每次遍历都会调用回调,退货值只是被忽略,循环不受影响。如果你真的需要提前退出,可以用for...ofsomeevery。比如:

const arr = [1, 2, 3, 4]; let found = false; arr.some((v) => { if (v === 3) { found = true; return true; // 提前终止 } });

但要注意,用some做“提前退出”有点滥用其语义,更好的方式是for...ofbreak。不过面试中问“如何在forEach里实现break”,通常会考你try...catch抛异常的方式,但那并不优雅,且会破坏控制流,不建议在生产中使用。

最后是修改原数组的问题。forEach规范里有一个非常隐蔽的行为:遍历过程中如果增加了数组元素,新增元素可能会被访问到;如果删除了某个元素,该位置可能被跳过;如果改变了元素值,实际被回调读取到的可能是修改后的值。这导致结果可能不符合直觉。官方建议:不要指望遍历过程中数组不被修改,也不要在回调里修改数组除了“修改当前元素”以外的部分。

举个例子:

const arr = [1, 2, 3, 4]; arr.forEach((v, i) => { console.log(v); if (i === 1) { arr.push(5); // 新增元素,原生的forEach会继续访问arr[4] } }); // 输出 1 2 3 4 5

而如果你在索引为1时执行arr.shift(),则后面的索引会前移,导致一些元素被跳过。这类问题非常隐蔽,一般只有在处理动态列表时才可能遇到。我的建议是:如果需要在遍历时删除或添加元素,优先使用filter、map等返回新数组的方法,或者使用for循环从后往前遍历。

3. filter的实现:筛选逻辑背后的机理

3.1 filter的语义与典型应用

filter的核心语义是“留下满足条件的元素”,并且返回一个新数组,不修改原数组。它的API签名是:

arr.filter(callback(element [, index [, array]]) [, thisArg]);

forEach相比,filter有两个显著区别:第一,它返回一个新数组,包含所有回调返回true(或真值)的元素;第二,空位也会被跳过,不参与判断。

典型应用是数据筛选。比如从一个用户列表中筛选出所有成年人:

const adults = users.filter((user) => user.age >= 18);

再比如从一组商品中筛选出有库存且价格低于100元的:

const affordable = products.filter((p) => p.inStock && p.price < 100);

它非常契合函数式编程的“声明式”风格:你只关心“筛选条件”,不需要关心循环怎么走。这也是为什么我写业务代码时,能不用for循环就不用,因为filter的表达力更强,bug更少。

3.2 手写filter:保留满足条件的元素

直接给出实现:

Array.prototype.myFilter = function(callback, thisArg) { if (this == null) { throw new TypeError('Array.prototype.myFilter called on null or undefined'); } if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } const arr = Object(this); const len = arr.length >>> 0; const result = []; let k = 0; while (k < len) { if (k in arr) { const value = arr[k]; if (callback.call(thisArg, value, k, arr)) { result.push(value); } } k++; } return result; };

这个实现和myForEach非常像,比较关键的差异在返回值。filter必须创建并维护一个result数组,当回调返回真值时把当前元素value压入结果。

这里有几个容易忽略的细节:

一是回调的判断条件if (callback.call(...)),它并不要求必须返回布尔值,只要是真值就会被保留。这符合JS的“truthy/falsy”规则,比如{}[]1'a'都会保留元素;0''nullundefinedNaNfalse都会丢弃。如果你想严格要求布尔值,可以在回调里用Boolean()包裹,但原生filter不做这个转换。

二是拿到value存成局部变量,是有原因的。因为callback.call执行时可能会改变arr[k]的值,而filter保留的是执行回调前已经获取的值,也就是value。如果用arr[k]去push,可能push进去的是修改后的新值。规范的意图是保留原数组里对应位置的值(在进入回调那一刻的值),所以先取出来更安全。

三是if (k in arr)仍然不能省略。如果不跳过空位,filter会错误地处理稀疏数组。例如[1, , 3].filter(v => v === undefined),原生返回[],因为空位被跳过,没有一个元素被判断为undefined;而如果你的实现不检查k in arr,那么arr[1]会被当作undefined参与判断,结果就会错误地保留一个undefined

3.3 filter与forEach的组合:如何实现筛选后再处理

实际开发中,经常需要先筛选再遍历。比如,筛选出所有金额大于100的订单,然后累加总额:

const total = orders .filter((order) => order.amount > 100) .reduce((sum, order) => sum + order.amount, 0);

但有时候你不想用reduce,就想用forEach。这时候可以这样写:

let total = 0; orders .filter((order) => order.amount > 100) .forEach((order) => { total += order.amount; });

甚至可以直接手动实现一个“筛选后执行”的组合函数:

function filterAndForEach(arr, predicate, action, thisArg) { arr.myFilter(predicate, thisArg).forEach(action, thisArg); }

不过,如果数组很大,这种链式调用会创建中间数组,增加内存开销。性能敏感的场景可以考虑一次遍历完成筛选和处理:

let total = 0; for (const order of orders) { if (order.amount > 100) { total += order.amount; } }

在数据量几百上千时,这点性能差异可以忽略不计;但如果是几十万条数据,中间数组的分配和GC会成为瓶颈。我的建议是:优先写清晰的链式代码,只有在真实性能分析确认有问题时再去优化成单次循环

这里也引出一个与热词相关的小插曲:很多人一听到filter,会联想到音视频领域的卷积滤镜(convolution filter)、图形图像处理里的卷积核,或者Windows事件跟踪里的事件过滤器(比如event filter with query)。实际上,不同领域的filter都是“筛选、过滤”的意思。在JavaScript里,filter只是数组的高阶函数,用来筛选元素,跟那些底层滤镜没有任何关系。写代码时千万别混淆。

4. every的实现:全真判断的严谨性

4.1 every的语义与短路的秘密

every用于测试数组中的所有元素是否都通过了回调的测试。它返回一个布尔值。API签名如下:

arr.every(callback(element [, index [, array]]) [, thisArg]);

它的一个极其重要的特性是短路:只要回调有一个返回假值,every立刻停止遍历,直接返回false。这跟逻辑运算符&&很像:一假则假,后面的不用管了。正因为有短路行为,every在处理大数组时效率可能高于forEach,因为它不需要完整遍历。

另一个容易被忽略的规范是:对空数组调用every,永远返回true。这其实符合数学上的“全称命题”逻辑:对空集的任何断言都是真的,因为不存在反例。你可以这么理解:如果数组里没有任何元素,那自然也就没有“不满足条件的元素”,所以通过了测试。这点和some正好相反,some对空数组返回false,因为空集中不存在一个满足条件的元素。

4.2 手写every:如何准确判断空数组

实现如下:

Array.prototype.myEvery = function(callback, thisArg) { if (this == null) { throw new TypeError('Array.prototype.myEvery called on null or undefined'); } if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } const arr = Object(this); const len = arr.length >>> 0; let k = 0; while (k < len) { // 跳过稀疏数组空位 if (k in arr) { // 只要有一个假值,立即返回false,实现短路 if (!callback.call(thisArg, arr[k], k, arr)) { return false; } } k++; } // 全部通过(包括空数组),返回true return true; };

重点在最后一行:如果len为0,while循环一次都不会执行,自然走到return true。这和原生的行为一致。

另外,if (k in arr)中,如果遇到空位,我们需要跳过空位继续检查后面的元素。这是不是意味着空位会被“无视”?是的,原生every会跳过空位,不调用回调,也不把它当作“不满足条件的元素”。所以:

[1, , 3].every((v) => v !== undefined); // true

因为空位被跳过,剩下的1和3,以及回调判断v !== undefined,两个元素都满足,所以结果true。如果你手动用for循环遍历时会访问到空位的undefined,结果就会变成false。在实现中保留k in arr判断非常重要。

手写实现还有一个细节:回调执行次数。因为存在短路,every中回调可能不会被执行完。比如:

[1, 2, 3].myEvery((v) => { console.log(v); // 只会打印1,因为1是假值?不,这里v=1是真值,所以会继续。如果条件是v > 0,则全真。 });

如果你写v => v < 3,那么对于[1,2,3],会先看1,返回true;再看2,返回true;最后看3,返回false,立即终止,不会再看后面的元素。这个“只执行必要次数”的特性能用来做条件判断的优化。

4.3 some与every的对照,以及综合应用案例

要理解every,最好和some一起说。some判断数组中是否至少有一个元素满足条件,它同样有短路行为:只要一个为真,立即返回true;空数组返回false

可以这样对照记忆:

方法语义空数组返回值短路条件
every所有元素都满足true遇到第一个假值返回false
some至少一个元素满足false遇到第一个真值返回true

业务中最常见的综合应用是“多条件校验”。比如,一个注册表单页,需要验证所有必填字段都不能为空:

const fields = ['username', 'email', 'password']; const isValid = fields.every((field) => { const value = form[field]; return value !== undefined && value !== ''; });

再比如,权限系统中判断用户是否拥有所有指定权限:

const requiredPermissions = ['read', 'write', 'delete']; const hasAll = requiredPermissions.every((perm) => user.permissions.includes(perm));

还有一个小技巧:如果every的回调会产生副作用,比如往外部数组里push内容,要警惕短路导致副作用不完整。举个极端例子:

let count = 0; const result = [1, 2, 3].every((v) => { count++; return v < 2; }); console.log(result); // false console.log(count); // 2,因为第二个元素就返回false,第三个元素并没有被执行

所以,如果你依赖回调“必须对所有元素执行”来收集数据,就不要用every,这种情况应该用forEach

5. 基于这些实现,我们还能做什么扩展?

5.1 自定义高阶函数:map、reduce、find的衍生

有了forEachfilterevery的手写经验,你可以很轻松地扩展出其他高阶函数。

比如map,它的实现和filter很像,只是把“满足条件才push”改成“无条件push回调的返回值”:

Array.prototype.myMap = function(callback, thisArg) { if (this == null) throw new TypeError(...); if (typeof callback !== 'function') throw new TypeError(...); const arr = Object(this); const len = arr.length >>> 0; const result = new Array(len); let k = 0; while (k < len) { if (k in arr) { result[k] = callback.call(thisArg, arr[k], k, arr); } k++; } return result; };

注意,map会保持原数组的稀疏结构:空位在新数组里仍然是空位,而不是undefined。这一点很有趣,但很多人不知道。

再比如find,它需要返回第一个满足条件的元素值,没有则返回undefined。它的实现也可以用every的短路思路,不过终止条件是“找到真值”:

Array.prototype.myFind = function(callback, thisArg) { if (this == null) throw new TypeError(...); if (typeof callback !== 'function') throw new TypeError(...); const arr = Object(this); const len = arr.length >>> 0; let k = 0; while (k < len) { const value = arr[k]; if (k in arr && callback.call(thisArg, value, k, arr)) { return value; } k++; } return undefined; };

这些实现共同的核心模式是“遍历数组 + 回调驱动 + 边界处理”。一旦建立起这个模式,你甚至可以去实现reduceflatMapgroupBy等更复杂的方法。

5.2 函数式编程与链式调用的性能考量

当你可以手写这些方法后,你对链式调用的理解会发生质变。比如这样一段代码:

const result = users .filter(user => user.isActive) .map(user => user.score) .filter(score => score > 60) .reduce((sum, score) => sum + score, 0);

看起来非常优雅,但它的执行过程实际上是:filter生成一个新数组,map生成一个新数组,第二个filter又生成一个新数组,最后reduce累加。每一步都会分配内存、遍历数组。

在小数组上这不是问题,但在处理数万条数据时,中间数组的创建会带来明显的内存压力。如果你追求极致性能,可以手写一个“流水线”函数,让数据只遍历一次:

const result = users.reduce((acc, user) => { if (!user.isActive) return acc; const score = user.score; if (score > 60) return acc + score; return acc; }, 0);

这个版本只遍历一次,不产生中间数组。代价是代码的可读性稍微下降。在实际项目中,我的原则是:可读性优先,先写出意图清晰的代码,再用性能工具(如Chrome Performance面板)找出真正的热点,不要凭空优化。

另外,如果你经常写链式调用,可以封装一些可复用的小函数,比如pipe

const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input); const process = pipe( arr => arr.filter(user => user.isActive), arr => arr.map(user => user.score), arr => arr.filter(score => score > 60), arr => arr.reduce((sum, score) => sum + score, 0) );

这样写的好处是逻辑分块清晰,维护起来也方便。

5.3 并发控制、异步场景中的这些方法

很多人会问:forEach可以配合async/await吗?可以,但要小心。如果你的回调是异步函数,forEach不会等待异步操作完成,它会直接遍历下一个元素,然后返回。来看一个例子:

async function processItems(items) { items.forEach(async (item) => { await someAsyncTask(item); }); console.log('done'); // 可能先于异步任务执行完打印 }

这是因为forEach不会等待回调返回的Promise。如果你需要串行执行异步任务,用for...of配合await

async function processItems(items) { for (const item of items) { await someAsyncTask(item); } console.log('done'); // 等所有任务执行完后打印 }

如果你想并发执行,可以用Promise.all配合map

await Promise.all(items.map((item) => someAsyncTask(item)));

但要注意,map在这里仍然会同步遍历所有元素,立即发起所有异步任务,然后再等待全部完成。如果并发数过大,可能会压垮服务器,一般需要做并发控制(比如分批处理)。

filterevery在异步场景中同样有自己的局限。filter的回调如果是异步函数,它返回的是Promise对象,而Promise对象永远是truthy,所以filter(async () => true)会把所有元素保留,而不是按异步结果筛选。正确做法是:

const results = await Promise.all( items.map(async (item) => { const keep = await check(item); return keep ? item : null; }) ); const filtered = results.filter(Boolean);

或者使用现代写法:

const filtered = []; for (const item of items) { const keep = await check(item); if (keep) filtered.push(item); }

every在异步环境下也不能直接用async回调,因为回调返回的Promise永远被当作truthy,结果会恒为true。如果你需要异步全真判断,也得手动for...of遍历。这些都是在实际开发中容易踩的坑,尤其在后端Node.js处理数据时经常遇到。

6. 常见问题与排查技巧实录

6.1 为什么不能用return跳出forEach?

前面已经提到过,forEach忽略了回调的返回值,它不像everysome那样会检查返回值。如果你在回调里写return false,只会结束当前这次回调的执行,不会终止整体遍历。

排查类似问题时,先确认你是不是在forEach里写了“提前退出”逻辑。如果是,建议改成for...ofbreak,或者改用some/every(根据条件语义选择)。还有一种特殊情况:你希望“跳过当前元素但继续后续元素”,这本来就不需要continue,只要在回调里用if包住逻辑即可。注意forEach没有continuebreak,只有return是合法的“跳过当前回调”方式,但语义上相当于continue而不是break

如果你硬要在forEach中模拟break,用try...catch抛出自定义异常是常见的“面试解法”,但生产环境不推荐。它会让控制流变得难以追踪,还会吞掉其他异常,除非万不得已,别这么做。

6.2 filter通常不会修改原数组,但回调里的副作用要注意

filter本身不会修改原数组,它返回一个新数组。但如果你在回调里间接修改原数组元素、增删数组项,就会产生不可预测的结果。

比如:

const arr = [1, 2, 3, 4]; const result = arr.filter((v, i) => { if (i === 1) { arr[2] = 30; // 修改后续元素 } return v > 2; }); console.log(result); // [3, 30]

因为遍历到第0个元素时,v是1,不满足条件;遍历到第1个元素时,v是2,不满足,但修改了arr[2]为30;遍历到第2个元素时,v已经变成30,满足条件,被保留。这导致你看到的结果可能和预期完全不一致。

排查思路:不要在filterforEachevery的回调里修改数组的结构(使用pushpopspliceshiftunshift等)。如果确实需要,考虑先复制一份数组,或者用“从后往前”的循环方式。此外,如果你看到filter调用了但原数组长度变了,那肯定是回调里有副作用,逐一检查回调内部逻辑即可。

顺带提一下,热词里有一个“dumpcs filter”,很多人第一眼以为是什么高级的筛选函数,其实它可能指某个工具或脚本里的filter模块。这种命名上的混淆在搜索时经常出现。JavaScript的filter只是数组方法,别被其他领域的同名术语带偏。

6.3 every在空数组上的行为,以及如何测试?

空数组调用every返回true,这是规范行为,不是bug。如果你希望“空数组时校验不通过”,需要在调用every前手动判断数组长度:

const canSubmit = fields.length === 0 ? false : fields.every(isValid);

或者封装一个函数:isEmpty判断先行。测试的时候也要把空数组列入测试用例。我见过同事因为不知道这个行为,在表单校验场景中被空数组导致“所有必填字段为空也能通过”,最后排查了半天才找到原因。

写单元测试时的经验是,最好把边界情况列成表格,一次性校验清楚。比如针对myEvery

输入数组测试条件期望结果
[]v => v > 0true
[1, 2]v => v > 0true
[1, 2]v => v > 1false
[1, , 3]v => v > 0true

最后一行说明空位被跳过,所以两个真实元素都满足,结果true。这个表格能帮你快速验证实现的正确性。

如果你在写手写实现,也可以用这些用例来测试你的myEvery。同理,myFiltermyForEach也需要类似的边界用例:包含空位的数组、非数字索引、类数组对象、字符串作为调用者等等。

最后,再分享一个我在实际项目中用过的小技巧:如果你经常需要“校验数组非空且全部满足条件”,可以这样写:

const isValid = arr.length > 0 && arr.every(check);

这样既避免了空数组的陷阱,又保持了代码简洁。而且,由于&&的短路特性,当arr.length === 0时,根本不会调用every,性能也不会浪费。

说实话,这三个方法我写了无数次手写实现,但每次都能发现新的细节。尤其是稀疏数组、回调副作用、空数组返回值这几块,几乎就是面试官最爱挖的坑。你可以把我上面的代码拷贝到自己项目里,跑几个测试用例,亲手感受一下这些边界情况。等你真正理解了它们的行为,不仅面试能对答如流,日常写代码也能少掉很多坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 17:01:14

支付网关PCI DSS 4.0合规自动化测试实战与落地指南

上个季度&#xff0c;我被拉进支付网关年审支援小组&#xff0c;任务是从测试视角协助安全团队完成PCI DSS 4.0合规检查。说是协助&#xff0c;实际就是对着几十页检查表逐项打勾&#xff1a;TLS版本有没有升级、登录失败有没有锁定、会话超时是不是15分钟、日志有没有留够一年…

作者头像 李华
网站建设 2026/9/9 17:00:17

2026年9月宜宾代账报税出错的原因有哪些资深会计这样分析

到了2026年&#xff0c;宜宾的创业氛围越来越浓&#xff0c;新注册的小微企业和个体户数量持续攀升。但与此同时&#xff0c;报税出错、申报逾期、税企沟通不畅这类消息&#xff0c;也隔三差五地在生意人圈子里冒出来。明明只是找个代账公司把每月的账报了&#xff0c;怎么还会…

作者头像 李华
网站建设 2026/9/9 16:57:26

Linux服务器部署开源大模型:从环境准备到上线调优全攻略

大模型这个东西&#xff0c;前两年还只是论文里的概念&#xff0c;今年已经变成很多公司和个人开发者手里的常规工具了。尤其是开源模型的崛起&#xff0c;类似Qwen、Llama、DeepSeek这些模型权重全部开放&#xff0c;让"自己部署一个私有大模型"从极客折腾变成了完全…

作者头像 李华
网站建设 2026/9/9 16:56:56

日志分析新思路:用AI单词聚合实现日志自动巡检与摘要

1. 这个项目到底解决了什么问题 先说说我自己的经历。以前我每天早上到工位的第一件事&#xff0c;就是打开终端翻日志&#xff1a;昨晚有没有报错、哪个接口超时了、Redis 有没有异常、有没有慢查询半夜把数据库拖垮。这一套流程熟练之后&#xff0c;五分钟内能扫完&#xff0…

作者头像 李华
网站建设 2026/9/9 16:52:11

非科班转码Python大数据:避开弯路的完整学习路径

1. 转码前先把账算清楚&#xff1a;非科班的短板从来不是编码我在过去的项目、带人经历中见过太多非科班转码的案例&#xff0c;其中有成功的&#xff0c;也有中途放弃的。一个很扎心的观察是&#xff1a;非科班转码者最大的障碍&#xff0c;往往不是"学不会"&#x…

作者头像 李华