iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 17
4
Modern Web

重新認識 JavaScript系列 第 17

重新認識 JavaScript: Day 17 函式裡的「參數」

本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。

購書連結 https://www.tenlong.com.tw/products/9789864344130

讓我們再次重新認識 JavaScript!


過去我們花了將近一週的時間介紹了瀏覽器裡的 JavaScript,也知道了 JavaScript 實際上是透過 BOM、DOM API 來與瀏覽器打交道。 相信看過了前面幾天的文章後,你已經對 DOM API 以及網頁事件有了基本的認識。

接著,如同我在系列文前言的預告,在接下來的分享中,要進入到「深入探討 JavaScript 篇」囉!

這系列的重點會著重於 JavaScript 語言的核心概念:「函式、物件、原型鍊」,與各位一同更深入地探討 JavaScript 核心的部份。


一級函式 (First class functions)

Day 10 函式 Functions 的基本概念 一文中,我們不斷地強調一個概念「函式是物件的一種」。 那麼,也許你也聽過這種說法:在 JavaScript 裡,「函式是第一級公民」。

第一級公民」? 什麼意思?

「第一級公民」指的是你可以將函式存放在變數、物件以及陣列之中,同時,你也可以將函式傳遞到函式,或者由另一個函式來回傳它。 而且,函式具有屬性,因為它實際上是一個「物件」。

換句話說,任何你對其他類型可以做的事,你都可以對「函式」做。

// 把函式存入「變數」, 呼叫時執行 funcA();
var funcA = function(){};

// 把函式存入「陣列」, 呼叫時執行 funcB[0]()
var funcB = [function(){}];

// 把函式存入「物件」的屬性, 呼叫時執行 funcC.method() 或 funcC['method']()
var funcC = { method: function(){} };
// 把函式當作「參數」,傳入到另一個函式中。
var funcD = function(func){
  return func;
};

// 「另一個」函式: 存放的是 funcD,而參數為一個匿名函式
var runFuncPassedToFuncD = funcD(function(){console.log('Hi');});

// 呼叫「另一個」函式
runFuncPassedToFuncD();   // "Hi"
// 函式是「物件」的一種,當然也可以有自己的「屬性」
var funcE = function(){};
funcE.answer = 'yup';

console.log(funcE.answer);      // 'yup'

當我們透過 typeof 去檢查某個函式的時候,雖然你會得到 "function" 的結果,讓你以為 function 也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object 的一種。

函式是一種物件,值也是物件。 你可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件 (值)。


函式的參數

之前我們說過,當我們呼叫一個函式的時候,可以透過「函式名稱」加上「小括號」的方式呼叫。 而小括號內的資料,就是「參數」。

var plus = function (numA, numB) {
  return numA + numB;
};

plus(1, 2);      // 3
plus(3, 4);      // 7

像上面這樣,呼叫 plus(1, 2) ,其中的 1, 2 作為參數傳至 plus 這個 function,這時 numAnumB 內的值就會分別是 12

於是,回傳的內容 numA + numB 自然就會是 1 + 2 的結果了。

然而,即便我們定義函式時有指定「參數的數量」(如上面範例,分別為 numAnumB),但是在呼叫的時候,並不會針對代入的參數數量做檢查。

也就是說,呼叫 plus 你可以寫成:

plus(1, 2, 3, 4, 5);

或是

plus( );

在 JavaScript 都是合法的,只不過在沒有傳入值作為參數的情況下,那些沒有指定的參數預設會是 undefined。 而多傳入的那些參數,在「大部分」情況下是沒有意義的。

既然我說了「大部分」那就代表還是可以拿得到的。


arguments 物件

事實上,當函式被呼叫的時候,會產生一個 arguments 物件。 而這個 arguments 物件的內容,其實就是我們呼叫函式所代入的「參數」。

以剛剛的 plus 作為範例:

plus(1, 2, 3, 4, 5);

很明顯我們代入的參數數量超過了先前定義好的參數數量,那麼多餘的 3, 4, 5 我們有辦法可以取得嗎?

可以,就透過 arguments 這個物件。

var plus = function (numA, numB) {
  console.log( arguments.length );

  return numA + numB;
};

// 因為有 5 個參數,會先 log 出 5,然後回傳 1+2 的結果
plus(1, 2, 3, 4, 5);

請注意, arguments 雖然看起來像個「陣列」,但實際上他只是個帶有「索引」特性的物件,然後內建個 length 屬性,其他地方與「陣列」完全不同,當然也沒有 .map().filter() 這些陣列才有的方法。 [註1]

所以說,即便在定義函式的時候完全沒有指定參數給它,我們仍然可以在函式內透過 arguments 來取得參數。

var plus = function (numA, numB) {

  for( var i = 0; i < arguments.length; i++ ){
    console.log( arguments[i] );
  }

  return numA + numB;
};

// console.log 印出 1 2 3 4 5
plus(1, 2, 3, 4, 5);

除此之外, arguments 物件還有另一個屬性: callee,指的是目前執行的函式。

var plus = function (numA, numB) {

  // arguments.callee 指的是 plus 這個 function
  console.log( arguments.callee );

  return numA + numB;
};

當我們需要在函式執行「遞迴」 (在函式內自我呼叫) 時,可以執行 arguments.callee() 來達成,這屬性在「匿名函式」時特別有用。 但要小心的是,在「嚴格模式」下不允許存取 arguments.callerarguments.callee 這兩個屬性。

另外, ES6 的箭頭函式 (Arrow Function) 也沒有提供 arguments 物件。


以「物件」作為參數

除了我們可以透過 arguments 去取得超出宣告數量的參數外,另外也有一種常見的方式:將多個參數用一個「物件」包裝起來

假設我們要做一個「將某人加入通訊錄」的功能,那麼就用一個叫 addPerson() 的函式來實作吧!


首先要有「姓名」、「電話」:

var addPerson = function(firstName, lastName, phone){
  // 略...
};

看起來很 ok 呢!

這時候 PM 大人來了,要求要可以加入 email 欄位:

// 加入了 email
var addPerson = function(firstName, lastName, phone, email){
  // 略...
};

但是,這時候問題又來了!

客服表示有使用者 英文不好 眼睛不好,在輸入資料後「Michelle」與「Michael」傻傻分不清楚,要求加入「性別」的欄位:

// 加入了 gender
var addPerson = function(firstName, lastName, phone, email, gender){
  // 略...
};

然後又多了奇奇怪怪的需求,加入「生日」、「地址」等等...

// 又加入了各種欄位
var addPerson = function(firstName, lastName, phone, email, gender, birthday, address){
  // 略...
};

於是最後工程師在呼叫這個 addPerson() 的時候整個大崩潰。

addPerson('Kuro', 'Hsu', '0987654321', 'kurotanshi@gmail.com', 'male', null, 'Taipei City');

順序不能錯,參數不能漏,一個蘿蔔一個坑。
只要中間少了一個參數,你的通訊錄欄位就整個 gg 了。

那麼這個時候,改用「物件」的方式來取代這一堆參數,就會是很簡便的做法:

var people = {
  firstName: 'Kuro',
  lastName: 'Hsu',
  phone: '0987654321',
  email: 'kurotanshi@gmail.com',
  gender: 'male',
  address: 'Taipei City'
};

// 最後把 people 物件作為參數傳入 addPerson
addPerson(people);

像這樣,不僅呼叫函式變得更加簡便,而且由於物件的屬性不要求「順序」,所以就算中間忽略掉幾個非必填的屬性也沒問題,使得程式碼更容易閱讀,也易於維護。 往後就算要新增參數也不用擔心影響到過去的程式。


參數的預設檢查

前面說過,在 JavaScript 中,即使函式的參數數量已經定義過,但實際在呼叫的時候仍然可以不傳參數或者傳入不對等的數量。

傳多了還無所謂,傳少了那些接不到值的參數們就會變成 undefined

那麼,有個很實用的檢查法,就是透過我們在 Day 08 運算式與運算子 [3]: Boolean 的真假判斷 曾介紹過的 || (OR) 這個運算子來幫助我們處理。

var plus = function (numA, numB) {
  return numA + numB;
};

plus 這個範例中,要是我們呼叫時,只帶入了一個參數:

plus(1);    // NaN

那麼 numB 就會變成 undefined,加總後的結果就是 NaN

為了要避免這種情況發生,我們可以改成這樣:

var plus = function (numA, numB) {
  numA = numA || 0;
  numB = numB || 0;

  return numA + numB;
};

當然我們知道,會被判斷成 false 的值不只 undefined,或是改用嚴謹一點的寫法:

var plus = function (numA, numB) {
  numA = (typeof numA !== 'undefined') ? numA : 0;
  numB = (typeof numB !== 'undefined') ? numB : 0;

  return numA + numB;
};

那麼這時,即便我們呼叫時只給定一個參數 plus(1); 最終得到的結果至少也會是 1 而不是 NaN 了。

另外,在 ES6 之後,我們也可以像這樣替參數指定預設值:

var plus = function (numA = 0, numB = 0) {
  return numA + numB;
};

也可以達到一樣的效果。


[註1] : 雖然 arguments 物件並非是陣列類型,但仍然可以透過 slice 或是 ES6 的 Array.from 來將它轉成一個新的陣列。

var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);

// ES6
const args = Array.from(arguments);

[註2]:在使用 function 傳遞參數時,要小心 「Pass by sharing」帶來的誤解。

var o = { value: 10 };

function changeValue(obj) {
  obj = { value: 123 };
}

changeValue(o);
console.log(o);   // 此時 o 仍是 { value: 10 }

詳情可參閱本系列 重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」? 一文


那麼,以上就是針對函式的參數所做的說明。


上一篇
重新認識 JavaScript: Day 16 那些你知道與不知道的事件們
下一篇
重新認識 JavaScript: Day 18 Callback Function 與 IIFE
系列文
重新認識 JavaScript37

尚未有邦友留言

立即登入留言