如何在 forEach 遇到特定 key:value 后停止循环?
结论
Array.prototype.some() 适合判断“数组中是否至少存在一个 Unit Volume 为 0 的产品”。找到匹配项后,它会立即停止检查并返回 true。
这段代码有两个问题。forEach 无法中途退出,而且循环中的每个产品都会直接修改 $scope.bpTree.response.errorCheckVolume。某个产品将它设为 true 后,后面的正常产品可能又将它改为 false,所以最终状态取决于最后一次赋值。
可以按下面的方式处理:
- 在循环中只设置当前产品的错误状态。
- 统计有错误的产品数量。
- 检查完所有产品后,再给
errorCheckVolume赋值一次。
每次单击 Next 时,代码都会根据当前数据重新计算结果,后续迭代也不会覆盖已经发现的错误。
推荐修改方式
function validateAllPlansUnitVolume(triggeredFromNextBtn) {
var key =
$scope.childProduct.instanceKey ||
$scope.childProduct.productName ||
$scope.childProduct.Name;
var coveragesData = $scope.sortedCoverages[key];
var errorRecords = checkForInvalidPlans(
coveragesData,
triggeredFromNextBtn
);
// 整个验证过程中只在这里设置一次全局状态
$scope.bpTree.response.errorCheckVolume = errorRecords > 0;
}
function checkForInvalidPlans(coveragesData, triggeredFromNextBtn) {
if (!Array.isArray(coveragesData) || coveragesData.length === 0) {
return 0;
}
var errorCount = 0;
coveragesData.forEach(function (product) {
var attributeRecords = [];
if (
product.attributeCategories &&
product.attributeCategories.records &&
product.attributeCategories.records[0] &&
product.attributeCategories.records[0].productAttributes &&
Array.isArray(
product.attributeCategories.records[0].productAttributes.records
)
) {
attributeRecords =
product.attributeCategories.records[0].productAttributes.records;
}
var hasInvalidUnitVolume = attributeRecords.some(function (attrNode) {
return (
attrNode.code === "ASC_Sales_Volume" &&
attrNode.userValues === 0
);
});
product.unitVolumeErr = hasInvalidUnitVolume;
product.unitVolumeErrMsg = hasInvalidUnitVolume
? "Unit Volume cannot be zero. Please update before proceeding to the next screen"
: "";
if (hasInvalidUnitVolume) {
errorCount += 1;
}
});
return errorCount;
}
这里用 some() 检查当前产品的属性。只要有一个属性同时满足下面两个条件,它就会返回 true:
attrNode.code === "ASC_Sales_Volume" &&
attrNode.userValues === 0
外层仍然使用 forEach,因为每个产品的 unitVolumeErr 和 unitVolumeErrMsg 都需要更新。如果外层也改用 some() 并提前结束,排在第一个错误产品之后的产品就不会重新验证,原先残留的错误标记也可能无法清除。
原代码为什么会得到错误结果
原代码在循环中反复执行:
$scope.bpTree.response.errorCheckVolume = true;
以及:
$scope.bpTree.response.errorCheckVolume = false;
假设有两个产品:
产品 A:Unit Volume = 0
产品 B:Unit Volume = 100
处理产品 A 时,状态会被设为 true;处理产品 B 时,又会被改成 false。循环结束后保留下来的是最后一次赋值,因此系统会允许用户进入下一页。
全局状态应该根据全部产品的验证结果来计算:
$scope.bpTree.response.errorCheckVolume = errorRecords > 0;
只判断是否存在错误
如果不需要给每个产品设置错误提示,只需要得到一个全局布尔值,可以在内外两层都使用 some():
function hasInvalidUnitVolume(coveragesData) {
if (!Array.isArray(coveragesData)) {
return false;
}
return coveragesData.some(function (product) {
var records =
product.attributeCategories &&
product.attributeCategories.records &&
product.attributeCategories.records[0] &&
product.attributeCategories.records[0].productAttributes &&
product.attributeCategories.records[0].productAttributes.records;
if (!Array.isArray(records)) {
return false;
}
return records.some(function (attrNode) {
return (
attrNode.code === "ASC_Sales_Volume" &&
attrNode.userValues === 0
);
});
});
}
$scope.bpTree.response.errorCheckVolume =
hasInvalidUnitVolume(coveragesData);
找到第一个错误后,这种写法会停止搜索,适合只判断“是否存在错误”的场景。
forEach 无法中途退出
forEach 没有内置的 break 机制。回调中的 return 只能结束本次回调,无法终止整个循环:
items.forEach(function (item) {
if (item.invalid) {
return; // 只跳过当前回调
}
});
如果需要提前停止遍历,可以按用途选择相应的方法:
some():找到满足条件的元素后返回true。find():找到后返回对应元素。every():遇到不满足条件的元素后返回false。for...of:可以使用break。
这个验证场景适合使用 some()。不建议通过抛出异常强行终止 forEach。
还需检查的细节
原代码在内层循环中声明了 attrNode,实际检查的却一直是:
unitVolumeAttrNodes[0].userValues
即使有多个匹配属性,代码也只会读取第一个。应该直接检查当前的 attrNode,也可以像推荐代码那样,用 some() 同时完成筛选和判断。
原代码中的 unitVolumeAttrNodes 也没有使用 var、let 或 const 声明:
unitVolumeAttrNodes = ...
在非严格模式下,它可能变成全局变量并被意外覆盖。至少应该改成:
var unitVolumeAttrNodes = ...
还需要确认 userValues 的实际类型。下面的严格比较只能匹配数值 0:
attrNode.userValues === 0
如果接口返回字符串 "0",条件会返回 false。类型不确定时,应先检查实际数据。确认业务规则允许转换后,可以显式转换:
Number(attrNode.userValues) === 0
需要注意,Number("") 也会把空字符串转换成 0,是否采用这种处理方式要根据实际业务规则决定。
最后,给出的代码没有使用 triggeredFromNextBtn。如果其他逻辑也不需要它,可以移除;如果 Next 按钮需要靠它区分验证时机,就应保留,并明确它的用途。
备注:内容仅供参考。
