JavaScript 令人惊讶的一点:对于空数组every()方法返回true

本文为翻译文章,原文为:JavaScript WTF: Why does every() return true for empty arrays? 是 《深入理解ES6》的作者Nicholas C. Zakas发布于2023年9月8日的文章。

JavaScript 语言的内核足够大,导致我们很容易误解它的某些部分是如何工作的。我最近重构了一些使用 every ()方法的代码,并且发现我并不真正理解every()的逻辑。在我看来,我认为回调函数必须被调用并返回 true的时候every() 才能返回 true,但事实并非如此。但是对于空数组,不管回调函数是什么,every ()都返回 true,因为根本不会调用该回调函数。看一下例子:

js 复制代码
function isNumber(value) {
    return typeof value === "number";
}

[1].every(isNumber);            // true
["1"].every(isNumber);          // false
[1, 2, 3].every(isNumber);      // true
[1, "2", 3].every(isNumber);    // false
[].every(isNumber);             // true

在此示例的每种情况下,均调用 every ()来检查数组中的每一项是否为数字。前四个调用相当简单,每个都会产生预期的结果。考虑如下的例子:

js 复制代码
[].every(() => true);           // true
[].every(() => false);          // true

这样的结果可能更令人感到惊讶: 对于every(),返回 true 或 false 的回调都具有相同的结果。发生这种情况的唯一原因是调用回调函数没有被调用,并且 every ()的默认返回值为 true。但是,当没有值可以用来运行回调函数时,为什么空数组对 every ()返回 true呢?

为了理解其中的原因,重要的是看看规范是如何描述这种方法的。

实现every()方法

ECMA-262定义了一个 Array.Prototype.every ()算法,该算法大致可以翻译成这段 JavaScript 代码:

js 复制代码
Array.prototype.every = function(callbackfn, thisArg) {
  const O = this;
  const len = O.length;
  if (typeof callbackfn !== "function") {
      throw new TypeError("Callback isn't callable");
  }
  let k = 0;
  while (k < len) {
      const Pk = String(k);
      const kPresent = O.hasOwnProperty(Pk);
      if (kPresent) {
          const kValue = O[Pk];
          const testResult = Boolean(callbackfn.call(thisArg, kValue, k, O));
          if (testResult === false) {
              return false;
          }
      }
      k = k + 1;
  }
  return true;
};

从代码中可以看出,every ()假定结果为 true,并且只有在回调函数对数组中的任何一项返回 false 时才返回 false。如果数组中没有元素,那么就没有机会执行回调函数,因此方法就没有办法返回 false。

现在的问题是:为什么every()要这样做?

数学和 JavaScript 中的全称量词

译者注:全称量词是指"所有"的概念,常用符号为∀。例如,对于集合S中的元素x,可以表示为∀x∈S,意为"对于S中的每一个元素x都成立"

MDN 提供了为什么 every ()对于空数组返回 true 的答案:

sql 复制代码
every 和数学中的全称量词"任意(∀)"类似。特别的,对于空数组,它只返回 true。
(这种情况属于无条件正确,因为空集的所有元素都符合给定的条件。)

无条件正确是一个数学概念,它意味着如果一个给定的条件(称为先行条件)不能被满足(也就是说,给定的条件是不真实的) ,那么某些东西就是真的。要将其应用到 JavaScript 中,那就是every ()对于空数组返回 true,因为没有办法调用回调函数 。回调代表了要测试的条件,如果由于数组中没有值而无法执行,那么 every ()必须返回 true。

全称量词是数学中一个更大的主题的一部分,这个主题被称为"全称量化",它允许你对数据集进行推理。考虑到 JavaScript 数组对于执行数学计算的重要性,特别是对于类型化数组,内置支持这种操作是有意义的。要知道,every()并不是唯一的例子。

数学和 JavaScript 中的存在量词

译者注: 存在量词是指"存在"的概念,常用符号为∃。例如,对于集合S中的元素x,可以表示为∃x∈S,意为"存在S中的一个元素x"

JavaScript 的some()方法实现了存在量词。"存在"量词指出,对于任何空集,结果都是 false。因此,some ()方法对于空数组返回 false,并且也不执行回调函数。下面是一些例子:

js 复制代码
function isNumber(value) {
    return typeof value === "number";
}
[1].some(isNumber);            // true
["1"].some(isNumber);          // false
[1, 2, 3].some(isNumber);      // true
[1, "2", 3].some(isNumber);    // true
[].some(isNumber);             // false
[].some(() => true);           // false
[].some(() => false);          // false

其他语言对量词的实现

JavaScript 并不是唯一的一种为集合或可迭代对象实现了量词相关方法的编程语言:

Python: all ()函数实现全称量词,而 any ()函数实现了存在量词。

Rust: Iterator: : all ()函数实现了全称量词,而 any ()函数实现了存在量词。

因此,JavaScript 与 every ()和 some ()都有着良好的合作关系。

意味着全称量词的every()

你是否认为evey()的行为是违反直觉的?这可有待商榷。然而,不管您的观点如何,您都需要了every()所具有的全程量词的性质以避免错误。简而言之,如果可能为空的数组使用了every (),则应该事先添加一个显式检查。例如,如果您有一个依赖于数字数组的操作,并且该操作将以空数组失败,那么您应该在使用 every ()之前检查该数组是否为空:

js 复制代码
function doSomethingWithNumbers(numbers) {
    // first check the length
    if (numbers.length === 0) {
        throw new TypeError("Numbers array is empty; this method requires at least one number.");
    }
    // now check with every()
    if (numbers.every(isNumber)) {
        operationRequiringNonEmptyArray(numbers);
    }
}

注意,只有当数组为空时不能执行某操作时,这样做是有必须要的。否则,可以避免这种额外的检查。

总结

虽然我对空数组上执行 every ()方法的执行结果感到惊讶,可是当我了解了某个操作的更大范围的上下文以及这种功能在各种语言之间的表现时,我就会感到释然。如果您也对这种行为感到困惑,那么我建议您在遇到every()的调用时改变你对它的理解。不再将 every ()读作"此数组中的每个项是否匹配此条件?"而是读作"数组中有没有不符合条件的项?"这种思维上的转变可以帮助您在以后的 JavaScript 代码中避免错误。

相关推荐
还是大剑师兰特8 分钟前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
m0_7482361116 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo61729 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489430 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356142 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink6 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-8 小时前
验证码机制
前端·后端
燃先生._.9 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js