一、前言
Hello~大家好。我是秋天的一阵风
在 JavaScript 开发中,every和some是我们日常处理数组时高频用到的两个数组方法,用法简单、逻辑直观,一直是前端处理数组判断的好帮手。但不少开发者在接触空数组的场景时,都会对一个现象感到困惑:
js
console.log([].every(item => item > 0)); // true
console.log([].some(item => item > 0)); // false
同样是空数组,调用两个逻辑相近的方法,结果却截然相反。这并不是 JavaScript 的设计漏洞,而是背后遵循了严谨的数学逻辑。
与其只记着 "空数组 every 返回 true、some 返回 false" 这个结论就完事,不如跟着这篇内容,从数学逻辑到手写源码,把这个知识点掰扯透。
我之前还写过一篇《给我十分钟,手把手教你实现 Javascript 数组原型对象上的七个方法》,里面把 forEach、map、reduce 这些常用数组方法的实现思路拆得明明白白,和这篇讲的内容是一个思路,看完这篇再去翻那篇,能把 JS 数组的底层逻辑摸得更透。
二、解开疑惑:这不是 JS 的特殊规定,是数学逻辑的体现
很多人第一次发现这个现象时,会觉得是 JavaScript 的特殊约定,其实不然,every和some的返回值逻辑,本质上是继承了数理逻辑中的量词规则,这也是这两个方法设计的底层依据。
1. every:对应全称量词的 "平凡真"
every的核心语义是 "数组中所有元素都满足某个条件" ,对应数学中的全称量词(∀) 。在数理逻辑里有个 "平凡真" 的概念,简单说就是:如果一个集合是空集,那么 "这个集合里所有元素满足某条件" 这个说法,本身是成立的。
举个通俗的例子,我们说 "空盒子里的所有苹果都是红的" ,因为盒子里根本没有苹果,也就不存在 "非红色的苹果" 来推翻这个说法,所以这个命题自然是真的。这也是[].every(...)会返回 true 的根本原因,是逻辑上的必然结果。
2. some:对应存在量词的 "平凡假"
而some的核心语义是 "数组中至少有一个元素满足某个条件" ,对应数学中的存在量词(∃) 。同理,空集合里没有任何元素,自然不可能找到满足条件的那个元素,就像说 "空盒子里有一个红苹果" ,显然是不成立的。所以[].some(...)返回 false,也是存在判断的必然结果。
光懂理论还不够,对于开发者来说,看得见的代码实现远比抽象的概念更易理解。接下来我们就用原生 JS 复刻这两个方法的核心实现,从代码层面看清楚背后的逻辑。
三、源码级拆解:兜底值,是结果不同的关键
ECMAScript 规范中,对Array.prototype.every和Array.prototype.some的执行逻辑有明确定义,我们复刻的核心实现完全贴合原生逻辑,这也是理解原生方法最直接的方式 ------ 亲手实现一遍,比看十遍文档更管用。
1. 复刻 Array.prototype.every
every的核心思路很简单:
- 先给一个 "真" 的初始兜底值,遍历数组时只要遇到一个不满足条件的元素,就立刻把结果置为假并终止遍历;
- 如果遍历完都没有反例,就保留初始的真。
js
Array.prototype.myEvery = function (callback, thisArg) {
// 校验回调函数的合法性
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = this;
const len = arr.length;
// 核心:初始兜底值设为true
let result = true;
// 空数组的len为0,会直接跳过这个循环
for (let i = 0;< len; i++) {
// 处理稀疏数组,跳过不存在的索引
if (!arr.hasOwnProperty(i)) continue;
// 执行回调并绑定this指向
const isPass = callback.call(thisArg, arr[i], i, arr);
// 有一个不满足,直接置假并终止遍历
if (!isPass) {
result = false;
break;
}
}
// 空数组直接返回初始的兜底值true
return result;
};
// 测试,和原生方法结果完全一致
console.log([].myEvery(item => item > 0)); // true
console.log([1,2,3].myEvery(item => item > 0)); // true
console.log([1,-2,3].myEvery(item => item > 0)); // false
从代码里能清晰看到,空数组因为长度为 0,会直接跳过遍历循环,最终返回一开始设定的兜底值 true,这就是空数组调用 every 返回 true 的代码实锤。
2. 复刻 Array.prototype.some
some的实现思路和every呼应,只是初始兜底值做了调整:先给一个 "假" 的初始兜底值,遍历数组时只要遇到一个满足条件的元素,就立刻把结果置为真并终止遍历;如果遍历完都没有正例,就保留初始的假。
js
Array.prototype.mySome = function (callback, thisArg) {
// 校验回调函数的合法性
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = this;
const len = arr.length;
// 核心:初始兜底值设为false
let result = false;
// 空数组同样会直接跳过循环
for (let i = 0; i< len; i++) {
// 处理稀疏数组,跳过不存在的索引
if (!arr.hasOwnProperty(i)) continue;
// 执行回调并绑定this指向
const isPass = callback.call(thisArg, arr[i], i, arr);
// 有一个满足,直接置真并终止遍历
if (isPass) {
result = true;
break;
}
}
// 空数组直接返回初始的兜底值false
return result;
};
// 测试,和原生方法结果完全一致
console.log([].mySome(item => item > 0)); // false
console.log([1,2,3].mySome(item => item > 5)); // false
console.log([1,6,3].mySome(item => item > 5)); // true
对比两个方法的实现代码,唯一的核心差异就是初始兜底值:
every以 true 为兜底,没遇到反例就一直为真;some以 false 为兜底,没遇到正例就一直为假。
空数组因为跳过了遍历,直接返回兜底值,这就是二者结果不同的根本原因。
四、实战避坑:开发中要注意的业务逻辑细节
理解了理论和源码,最终还是要落地到实际开发中。空数组的这个特性,在表单校验、列表筛选、数据判断等场景中,很容易因为忽略而引发小 bug,只要稍作处理就能避免。
典型场景:空列表的条件判断
举个电商开发的例子,我们需要校验购物车中的商品是否全部满足包邮条件(价格 > 100),满足的话就显示包邮按钮。如果直接写判断,就容易出问题:
js
// 考虑不周的写法:未判断数组是否为空
const cartList = []; // 用户还没加购任何商品
if (cartList.every(item => item.price > 100)) {
showFreeShippingBtn(); // 会执行!因为空数组every返回true
}
显然,用户购物车为空时,不应该显示包邮按钮,这就是把逻辑上的 "真",和业务上的 "合法" 搞混了。
正确解法:先校验数组非空,再做条件判断
无论使用every还是some,只要业务场景要求 "有数据的集合",就先判断数组的长度,再执行后续的条件校验,这是最稳妥的方式。
js
// 严谨的写法:先判断数组非空,再执行判断
const cartList = [];
if (cartList.length > 0 && cartList.every(item => item.price > 100)) {
showFreeShippingBtn();
} else if (cartList.length === 0) {
showEmptyCartTip(); // 给用户展示空购物车提示,体验更好
}
再比如用some判断列表中是否有过期优惠券,虽然空数组返回 false 本身符合 "没有过期优惠券" 的逻辑,但如果需要区分 "空列表" 和 "有列表但无过期",还是要单独判断:
js
const coupons = [];
if (coupons.some(item => item.isExpired)) {
showExpiredTip();
} else if (coupons.length === 0) {
showNoCouponTip(); // 空优惠券列表的专属提示
} else {
showAllValidTip(); // 有优惠券且都未过期的提示
}
总结
其实空数组下every返真、some返假的现象,一点都不复杂,总结起来就是两层核心逻辑:
- 数学层面 :
every是全称判断,空集合满足 "平凡真";some是存在判断,空集合满足 "平凡假",这是方法设计的底层依据; - 代码层面 :
every的初始兜底值为 true,some为 false,空数组会跳过遍历,直接返回兜底值。
希望看完这篇文章,你再遇到every和some的空数组场景时,能不再困惑,从容应对~