跳至主要內容

嚴格檢查函式呼叫的參數個數

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);

2 進制的 "10" 在 10 進制中為 2,2 進制的 "10.1" 在 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 個元數,但我們可以使用更多引數呼叫它,這很方便。

流程變更

我們認為已經找到一個折衷方案,既能捕捉到誘發錯誤,又不影響 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 編寫的存根和變數函式。但是,您可以使用 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 旗標?

這是一個相當核心的變更。如果我們保留這兩種行為,我們必須測試在有和沒有此變更的情況下,所有內容都能正常運作。隨著我們新增更多旗標,組合數會呈指數成長,而 Flow 的行為將變得更難以理解。因此,我們只選擇一種行為:嚴格檢查函式呼叫元數。

您怎麼看?

此變更是由 Flow 使用者的回饋所激勵。我們非常感謝我們社群中的所有成員,他們花時間與我們分享他們的回饋。這些回饋非常寶貴,有助於我們改善 Flow,因此請持續提供!