跳至主要内容

严格检查函数调用元数

Flow 的最初目标之一是能够理解惯用的 JavaScript。在 JavaScript 中,您可以调用一个函数,传递比函数预期更多的参数。因此,Flow 从未抱怨过调用一个函数时传递了多余的参数。

我们正在改变这种行为。

什么是元数?

函数的元数是指它期望的参数数量。由于某些函数具有可选参数,而某些函数使用剩余参数,因此我们可以将最小元数定义为它期望的最小参数数量,并将最大元数定义为它期望的最大参数数量。

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() 函数会将所有参数加起来,并且 sum 的值将为 4。但是,只有前两个参数被加起来,sum 实际上将具有值 2。这显然是一个错误,那么为什么 JavaScript 或 Flow 不会抱怨呢?

虽然上面的例子中的错误很容易发现,但在实际代码中,往往很难注意到。例如,这里 total 的值是多少

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

"10" 在 2 进制中是 10 进制中的 2"10.1" 在 2 进制中是 10 进制中的 2.5。因此,作者可能认为 total 将为 4.5。但是,正确答案是 12.1parseInt("10", 2) 确实计算结果为 2,如预期的那样。但是,parseFloat("10.1", 2) 计算结果为 10.1parseFloat() 只接受一个参数。第二个参数被忽略了!

为什么 JavaScript 允许多余的参数

此时,您可能觉得这只是 JavaScript 做出糟糕决定的一个例子。但是,这种行为在很多情况下非常方便!

回调

如果您不能调用一个函数,传递比它处理的更多的参数,那么遍历数组将看起来像这样

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

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

  1. 元素
  2. 元素的索引
  3. 您正在遍历的数组

但是,您的回调函数通常只需要引用第一个参数:元素。能够写成这样真的很棒

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

存根

有时我会遇到这样的代码

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

其想法是在开发环境中,调用 log() 会输出一条消息,但在生产环境中则什么也不做。由于您可以调用一个函数,传递比它期望的更多的参数,因此很容易在生产环境中存根 log()

使用 arguments 的可变参数函数

可变参数函数是指可以接受不定数量参数的函数。在 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 看起来不接受任何参数。因此,即使它看起来元数为 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 不会改变其函数子类型化行为。具有较小最大元数的函数仍然是具有较大最大元数的函数的子类型。这允许回调函数像以前一样工作。

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>) => number 的子类型。

存根和可变参数函数

不幸的是,这会导致 Flow 对使用 arguments 编写的存根和可变参数函数进行抱怨。但是,您可以通过使用剩余参数来修复这些问题

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 标志?

这是一个非常核心的更改。如果我们保留两种行为,我们必须测试所有内容是否在有和没有此更改的情况下都能正常工作。随着我们添加更多标志,组合数量呈指数级增长,Flow 的行为变得更难推理。出于这个原因,我们只选择一种行为:严格检查函数调用元数。

您怎么看?

此更改的动机来自 Flow 用户的反馈。我们非常感谢社区中所有抽出时间与我们分享反馈的人。这些反馈非常宝贵,帮助我们让 Flow 变得更好,所以请继续分享您的反馈!