本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
過去我們花了將近一週的時間介紹了瀏覽器裡的 JavaScript,也知道了 JavaScript 實際上是透過 BOM、DOM API 來與瀏覽器打交道。 相信看過了前面幾天的文章後,你已經對 DOM API 以及網頁事件有了基本的認識。
接著,如同我在系列文前言的預告,在接下來的分享中,要進入到「深入探討 JavaScript 篇」囉!
這系列的重點會著重於 JavaScript 語言的核心概念:「函式、物件、原型鍊」,與各位一同更深入地探討 JavaScript 核心的部份。
在 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,這時 numA
與 numB
內的值就會分別是 1
與 2
。
於是,回傳的內容 numA + numB
自然就會是 1 + 2
的結果了。
然而,即便我們定義函式時有指定「參數的數量」(如上面範例,分別為 numA
與 numB
),但是在呼叫的時候,並不會針對代入的參數數量做檢查。
也就是說,呼叫 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.caller
和 arguments.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 是「傳值」或「傳址」? 一文
那麼,以上就是針對函式的參數所做的說明。