iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 7
0
自我挑戰組

學JS的心路歷程系列 第 7

學JS的心路歷程 Day7-函式(二) arguments

引數 (argument) 與 函式參數 (parameter)

在討論函式時,很多人都會把這兩個搞混,我自己也不例外。
雖然講錯別人也聽得懂,但是我們還是要搞清楚這兩個的定義到底是什麼!

  • 引數是當我們呼叫函式時傳遞給它的值
  • 參數是我們在函式定義中所列出的變數

看完還是有點不懂?沒關係,上圖!

宣告式 (declaration) 與 表達式 (expression)

發現在前幾天介紹宣告函式時沒有特別說明兩者名稱的差異,雖然這跟上面的引數與參數一樣說錯別人也懂,但我們還是要分清楚喔!

宣告式

function funA(){
...
}

表達式

var myFun = function(){
...
}

隱含引數

在呼叫函式時候除了傳遞自己給的引數外,你知道還會傳遞兩個隱含參數: arguments 和 this 嗎?

什麼事「隱含」,就是這些參數沒有明確列在函式署名( function signature)中,只會默默傳遞給函式,並且可以在函式內存取他們,也可以在函式中被引用。

我們今天只會先介紹 arguments , this 會在之後的篇幅中提到。

arguments

是傳遞給函式的所有參數之集合,不用管相對應的參數是否有明確定義。這也讓我們可以實現 JS 本身不支援的函式重載 (function overloading) 及 可變參數數量的可變函式 (variadic function) 。

講到這邊可能會有人困惑說什麼是函式重載?所謂函式重載就是函式的名稱都一樣,但是會根據不同的輸入擁有不同的輸出。
看不太懂也沒關係,在我們講完 arguments 後會提到怎麼實作。

我們先來看一個簡單的例子:

function show(a,b,c){
    console.log(a,b,c);
    console.log(arguments);
    console.log(arguments[0]);
    console.log(arguments.length);
    console.log(typeof arguments);
}
show(1,2,3);

可以看到 arguments 儲存了所有傳遞參數的值。 而且還可以用lenght屬性,但這邊要注意到,它並不是一個陣列,是一個叫做類陣列的東西,也就是說無法使用 JS 提供給 array 的函式庫。

function show(a,b,c){
    arguments.sort(function(a,b){
        return a - b;
    });
}
show(1,2,3);//Uncaught TypeError: arguments.sort is not a function

這邊要注意到一點,如果我們修改了 arguments 的值,原本的參數也會被修改:

function modifyArg(a,b,c){
    console.log(a,b,c);//1 2 3
    arguments[0] *= 20;
    console.log(a,b,c);//20 2 3
}
modifyArg(1,2,3);

在前面有提到過說可以利用 arguments 作可變函式:

function dynamicArg(){
    console.log(arguments);
}
dynamicArg(1,2,3);//Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]

再來,我們來看一下到底怎麼作函式重載呢:

function mutli(){
    let argLength = arguments.length;
    if(argLength ===1){
        console.log(argLength[0])
    }else if(argLength ===2){
        console.log(argLength[0],argLength[1])
    }
    ...
}

這是最簡單的函式重載,雖然可以運作但實在看了頭有點痛。

我們今天來假設一個情境,我們想要有一個函式可以對人名進行搜索,根據不同的參數長度回傳搜尋的值,這時候我會就會需要一個 object 物件:

var people = {
    name:["Dean Edwards", "Alex Russell", "Dean Tom","Alex Tsai","Tom Cruise","Tom Ford"]
}

我們想要能find('Dean')時回傳 ["Dean Edwards","Dean Tom"];
find()時回傳所有人;find('Tom Cruise')時回傳["Tom Cruise"]

於是我們需要一個 函式:

var people = {
    name:["Dean Edwards", "Alex Russell", "Dean Tom","Alex Tsai","Tom Cruise","Tom Ford"]
}

function addMethod (obj, key, fn) {
  var old = obj[key];
  obj[key] = function () {
    if (fn.length === arguments.length) {
      return fn(arguments);
    } else if (typeof old === "function") {
      return old(arguments);
    }
  }
}

接著,我們就可以寫搜索人名的函式了:

//沒有參數傳入,回傳所有人
function findAll () {
  return people.name;
}

// 傳入一個參數時,回傳firstName相同的人名陣列
function findFirstName (firstName) {
  var ret = [];
  for (var i = 0; i < people.name.length; i++) {
    if (people.name[i].indexOf(firstName) !== -1) {
      ret.push(people.name[i]);
    }
  }
  return ret;
}

// 傳入兩個參數時,回傳firstName和lastName都相同的人名陣列
function findFullName (firstName, lastName) {
  var ret = [];
  for (var i = 0; i < people.name.length; i++) {
    if (people.name[i] === (firstName + " " + lastName)) {
      ret.push(people.name[i]);
    }
  }
  return ret;
}

寫完之後,將他們經由addMethod()傳入people物件裡面:

addMethod(people, "find", findAll);

addMethod(people, "find", findFirstName);

addMethod(people, "find", findFullName);

之後就可以用了:

console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom", "Alex Tsai", "Tom Cruise", "Tom Ford"]
console.log(people.find("Tom")); //["Tom Cruise", "Tom Ford"]
console.log(people.find("Dean Edwards")); //["Dean Edwards"]

或許有人會很困惑,為什麼我們可以這樣用,讓我們來逐一分析這個addMethod函式:

function addMethod (obj, key, fn) {
  var old = obj[key];
  obj[key] = function () {
    if (fn.length === arguments.length) {
      return fn(arguments);
    } else if (typeof old === "function") {
      return old(arguments);
    }
  }
}

可以看到我們每次呼叫時,都會先儲存原先的people.find到變數old,這時候就會造成閉包的現象,這我們後面篇幅會提到,簡單的說,我們可以藉由people.find不斷往上找,直到找不到為止。

所以當我們呼叫 people.find("Tom") 時,會執行:

if (fn.length === arguments.length) {
      return fn(arguments);
    } 
else if (typeof old === "function") {
      return old(arguments);
    }

這時候的fnfindFullName但由於傳入參數長度跟 findFullName 長度不相等,所以會執行:

else if (typeof old === "function") {
    return old(arguments);
}

這時候的old等於people.find("Tom") ,但是裡面包附的fn已經換成,
findFirstName 由於傳入參數長度跟 findFirstName 長度相等,便會直接執行,不會在往上找。

或許會有人認為,我為了寫一個函式重載,還得寫這麼多複雜的函式,不如需要時在呼叫就好了啊!

但是函式重載的好處在於,只需要苦過一次,後面就可以用短短的一行函式就得到想要的結果了!

參考資料:
忍者 JavaScript 開發技巧探秘
JavaScript中的函数重载(Function overloading)


上一篇
學JS的心路歷程 Day6-函式(一)
下一篇
學JS的心路歷程 Day8 -函式(三) this
系列文
學JS的心路歷程30

尚未有邦友留言

立即登入留言