严格检查函数调用的一致性

381 阅读4分钟

本文翻译自:Strict Checking of Function Call Arity

FLOW

FLOW IS A STATIC TYPE CHECKER FOR JAVASCRIPT.

译文正文

Flow 的最初目标之一是能够适应惯常的 JavaScript 书写格式。在 JavaScript 中,您可以使用比函数预期更多的参数来调用函数。Flow 也从不介意调用方传入无关参数调用函数。

但是现在我们正在改变这种做法。

什么是arity?

函数的arity是指该函数期望的参数的个数。一些函数具有optional parameters,一些具有rest parameters,所以我们可以在函数中定义最小的期望参数的个数,也可以在函数中定义最大的期望参数的个数。

function no_args() {} // arity of 0
function two_args(a, b) {} // arity of 2
function optional_args(a, b?) {} // min arity of 1, max arity of 2
function many_args(a, ...rest) {} // min arity of 1, no max arity

我们为什么改变想法?

读者可以考虑如下代码:

function add(a, b) { return a + b; }
const sum = add(1, 1, 1, 1);

很明显编程人员会认为 add() 函数是将其所有参数相加,并且得到的总和应该是 4。 但是, add() 函数只对前两个参数求和,最终得到的值为 2。这显然是一个bug,那么为什么 JavaScript 或 Flow 没有抱怨呢?

虽然上面示例中的错误很容易观察到,但在实际代码中通常很难观察。例如,这里对总价值 total 的计算:

const total = parseInt("10", 2) + parseFloat("10.1", 2);

二进制10的十进制为2,二进制10.1的十进制表示为2.5。某些编程人员可能认为 total 最终是4.5,但是最终得到的结果是12.1。parseInt("10", 2)的值确实是2,但是parseFloat("10.1", 2)的值是10.1。因为parseFloat()只接受一个参数,第二个参数被忽略!

为什么 JavaScript 允许额外参数

在这一点上,您可能会觉得这是 JavaScript 诞生以来最糟糕的决定。但是,这种行为在很多情况下都非常方便。

Callbacks 回调函数

如果一个函数不能够调用多余的参数,那么map方法的写法就会如下所示:

const doubled_arr = [1, 2, 3].map((element, index, arr) => element * 2);

当你调用 Array.prototype.map 时,你传入了一个回调函数。对于数组中的每个元素,都会调用该回调函数并传递 3 个参数:

  • The element
  • The index of the element
  • The array over which you’re mapping 但是,您的回调函数通常只需要第一个参数:element。如下所示:
const doubled_arr = [1, 2, 3].map(element => element * 2);

某一场景的适应代码

有时我会遇到这样的代码

let log = () => {};
if (DEBUG) {
  log = (message) => console.log(message);
}
log("Hello world");

上面的代码是想在开发环境中调用log函数的时候输出一个message,但是在生产阶段,该代码不会有任何作用。由于在调用函数的时候你可以使用比预期更多的参数,使用 FLOW 很容易在生产中调用log(message)时报错。

使用arguements实现可变参数的函数

可变参数函数是可以接受无限数量参数的函数。在 JavaScript 中编写可变参数函数的方法是使用arguments。如下例:

function sum_all() {
  let ret = 0;
  for (let i = 0; i < arguments.length; i++) { ret += arguments[i]; }
  return ret;
}
const total = sum_all(1, 2, 3); // returns 6

sum_all 似乎不需要任何参数。但是即使它的arity似乎为 0,我们也可以使用更多参数来调用它,这样是很方便的。

FLOW 做出的改变

我们认为我们已经找到了一种折衷方案,可以在不破坏 JavaScript 的便利性的情况下捕获参数不匹配的错误。

如果一个函数的参数最多允许为 N,那么如果你使用超过 N 个参数调用它的时候,Flow 将开始给出提示。

test:1
  1: const num = parseFloat("10.5", 2);
                                    ^ unused function argument
   19: declare function parseFloat(string: mixed): number;
                                  ^^^^^^^^^^^^^^^^^^^^^^^ function type expects no more than 1 argument. See lib: <BUILTINS>/core.js:19

函数子类型

Flow 不会改变它的函数子类型行为。具有比预期参数数量少的函数是具有预期参数数量的函数的子类型。因此这没有影响回调函数。下面的代码经过 FLOW 的检查不会报错。

class Array<T> {
  ...
  map<U>(callbackfn: (value: T, index: number, array: Array<T>) => U, thisArg?: any): Array<U>;
  ...
}
const arr = [1,2,3].map(() => 4); // No error, evaluates to [4,4,4]

本例中, () => number 是 (number, number, Array) => number 的子类型。

场景代码和可变参数函数

不幸的是,调用函数时参数数量过多和在不同场景下调用相同的函数,将会导致 FLOW 报错。不过可以使用rest参数来解决这个问题。

let log (...rest) => {};

function sum_all(...rest) {
  let ret = 0;
  for (let i = 0; i < rest.length; i++) { ret += rest[i]; }
  return ret;
}

即将推出的计划

Flow v0.46.0 将默认关闭严格的函数调用数量检验。它可以通过带有标志的 .flowconfig 启用。

experimental.strict_call_arity=true

Flow v0.47.0 将启用严格的函数调用参数,并且将删除experimental.strict_call_arity。

为什么在这两个版本中启用experimental.strict_call_arity?

第一个版本是为了想第二个版本过过渡。

为什么不保留experimental.strict_call_arity 标志?

这是一个非常核心的变化。如果我们保留这两种行为,我们就必须测试在true和false的情况下一切都正常。随着我们添加更多标志,组合的数量呈指数增长,Flow 的行为变得更难推理。出于这个原因,我们只选择了一种行为:严格检查函数调用的数量。

你怎么看?

此更改的动机是 Flow 用户的反馈。我们非常感谢我们社区的所有成员花时间与我们分享他们的反馈。这些反馈非常宝贵,可以帮助我们改进 Flow,所以请继续提供!

参考文章