之前電面有問到:“你知道一個函式的length是什麼嗎?”
因為沒看過,也沒碰到過使用場景,我沒答出來。後來查了下發現是指函式的引數個數,於是也就作罷了。不過今天恰巧碰到了使用場景,且發現之前的理解也有誤,於是就寫一篇短文分享一下。
今天在嘗試 render props 的各種擴充套件玩法,寫了個簡單的表格資料 CRUD 操作 Demo:CodeSandbox
寫Demo一般從所有操作都同步開始,最後再全改成非同步的情況。所以需要寫一個將所有同步函式(資料載入以及增刪改)都轉換成非同步函式的工具函式。我一開始寫得如下:
const delay = ms => new Promise(_ => setTimeout(_, ms));
// 打算從今開始儘量使用 async/await
const withRequest = func => async args => {
await delay(1000);
if (Math.random() > 0.3) {
func(...args);
} else {
message.info("操作失敗!");
}
};
// 等一秒之後 30% 失敗,70% 執行操作
複製程式碼
這是個所謂的 Curried(庫裡?柯里?咖哩?)函式,用於批量改造函式的函式,接受func為引數,返回改造好的func。明眼人應該已經發現錯在哪裡了,不過我沒有,於是走了一堆彎路,卻收穫不少。
用此函式包裹了我的一堆測試方法:
add = (a,b) => a + b
square = a => a * a
loadData = () => this.setState({ ... })
loadData = withRequest(this.loadData);
add = withRequest(this.add);
square = withRequest(this.square);
複製程式碼
立馬報錯跪了,於是我知道在沒有引數的 loadData 函式那裡跪了,並開始了我的求知之旅。
問題:
如何將任意個引數從上級函式傳遞給下級函式?
笨辦法解決:分情況討論
分情況討論的關鍵是:如何知道函式有幾個引數呢?毫無疑問我想到了fn.length
, 於是寫下:
const len = func.length
if(len === 0){
func()
} else if(len === 1) {
func(args)
} else {
func(...args)
}
複製程式碼
這對了嗎?答案是不對。
fn.length
的定義是:函式的形參個數。也就是函式定義時的引數個數,而不是函式實際接受的引數個數。比如
const add = (a,b) => a + b
add(1,2,3,4,5) // 3
add.length // 2
複製程式碼
而問題的情況,我們需要判斷的是函式接受的引數個數。這時候有一個方便的內建變數:arguments
function func1(a, b, c) {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // 3
}
func1(1, 2, 3);
複製程式碼
arguments 即為函式接收到的所有引數組成的(類)陣列。那麼用 arguments.length
替換所有 func.length
是否就對了呢?還是不對,arguments 有它的侷限性:
const func1 = (a, b, c) => {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // 3
}
func1(1, 2, 3);
// error: arguments is not defined
複製程式碼
箭頭函式沒有arguments。 同時注意到現在前端程式碼的箭頭函式會經過 babel 轉譯,產生的結果是 arguments 雖然不會 undefined,但會有各種怪異賦值。總之在箭頭函式裡別使用。
真正的解決辦法
剩餘引數 (Rest parameters)
const withRequest = func => async (...args) => {
await delay(1000);
if (Math.random() > 0.3) {
func(...args);
} else {
message.info("操作失敗!");
}
};
複製程式碼
這段程式碼裡出現了兩個 ...args
, 前者是剩餘引數,後者是陣列展開。兩者一個是收束,一個是展開。功能相反。
function fun1(...args) {
console.log(args.length);
}
fun1(); // 0
fun1(5); // 1
fun1(5, 6, 7); // 2
複製程式碼
剩餘引數語法將“剩餘”的引數收束到一個陣列中, 注意和 arguments 不一樣, 剩餘引數是一個真正的陣列。繞了一大圈,其實是忘記寫了三個點。不過也算真正理解了:
- Function.length
- arguments
- rest & spread
最後,讓我們用剩餘引數挑戰一個實用函式吧:
問題
寫一個callAll
函式,它接收任意數量的函式和任意數量的引數,如果作為引數的函式存在就用所有的引數呼叫那個函式。
const add = (a,b) => {console.log(a + b)}
const minus = (a,b) => {console.log(a - b)}
callAll(add, minus)(2,1)
// 3
// 1
複製程式碼
答案如下:
// 剩餘引數是一個真正的陣列,可以使用任何陣列方法
const callAll = (...fns) => (...args) => fns.forEach( fn => fn && fn(...args))
複製程式碼