從Function.length 與 Argument.length 區別談到如何傳遞任意個數引數

FateRiddle發表於2018-07-01

之前電面有問到:“你知道一個函式的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 不一樣, 剩餘引數是一個真正的陣列。繞了一大圈,其實是忘記寫了三個點。不過也算真正理解了:

  1. Function.length
  2. arguments
  3. 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))
複製程式碼

相關文章