我們常常需要在腳本的許多地方執行類似的動作。
例如,當訪客登入、登出,或是在其他地方時,我們需要顯示一則美觀的訊息。
函數是程式的主要「建構區塊」。它們讓程式碼可以在不重複的情況下被呼叫多次。
我們已經看過內建函數的範例,例如 alert(message)
、prompt(message, default)
和 confirm(question)
。但我們也可以自己建立函數。
函數宣告
若要建立函數,我們可以使用函數宣告。
它看起來像這樣
function showMessage() {
alert( 'Hello everyone!' );
}
function
關鍵字最先,接著是函數名稱,然後是在括號中的一系列參數(以逗號分隔,上例中為空,我們稍後會看到範例),最後是函數程式碼,也稱為「函數主體」,置於大括號中。
function name(parameter1, parameter2, ... parameterN) {
// body
}
我們的新函數可以用其名稱呼叫:showMessage()
。
例如
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();
呼叫 showMessage()
會執行函數程式碼。在這裡,我們會看到訊息兩次。
此範例清楚說明函數的主要目的之一:避免程式碼重複。
如果我們需要變更訊息或顯示方式,只要修改一個地方的程式碼即可:輸出訊息的函數。
局部變數
在函數內宣告的變數僅在該函數內可見。
例如
function showMessage() {
let message = "Hello, I'm JavaScript!"; // local variable
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- Error! The variable is local to the function
外部變數
函數也可以存取外部變數,例如
let userName = 'John';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, John
函數可以完全存取外部變數。它也可以修改外部變數。
例如
let userName = 'John';
function showMessage() {
userName = "Bob"; // (1) changed the outer variable
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // John before the function call
showMessage();
alert( userName ); // Bob, the value was modified by the function
僅在沒有局部變數時才會使用外部變數。
如果在函數內宣告了同名的變數,則會遮蔽外部變數。例如,在以下程式碼中,函數會使用局部 userName
。外部變數會被忽略
let userName = 'John';
function showMessage() {
let userName = "Bob"; // declare a local variable
let message = 'Hello, ' + userName; // Bob
alert(message);
}
// the function will create and use its own userName
showMessage();
alert( userName ); // John, unchanged, the function did not access the outer variable
在任何函數外部宣告的變數,例如上述程式碼中的外部 userName
,稱為全域變數。
全域變數可從任何函數看到(除非被局部變數遮蔽)。
盡量減少使用全域變數是一種良好的習慣。現代程式碼很少或沒有全域變數。大多數變數都存在於其函數中。不過,有時它們可以用於儲存專案層級資料。
參數
我們可以使用參數將任意資料傳遞給函數。
在以下範例中,函數有兩個參數:from
和 text
。
function showMessage(from, text) { // parameters: from, text
alert(from + ': ' + text);
}
showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)
當函數在行 (*)
和 (**)
中被呼叫時,給定的值會複製到局部變數 from
和 text
。然後函數使用它們。
以下是一個範例:我們有一個變數 from
,並將它傳遞給函數。請注意:函數會變更 from
,但變更不會在外部顯示,因為函數總是取得值的副本
function showMessage(from, text) {
from = '*' + from + '*'; // make "from" look nicer
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// the value of "from" is the same, the function modified a local copy
alert( from ); // Ann
當值作為函數參數傳遞時,也稱為引數。
換句話說,要將這些術語釐清
- 參數是函式宣告中括弧內列出的變數(這是宣告時間術語)。
- 引數是在呼叫函式時傳遞給函式的值(這是呼叫時間術語)。
我們宣告函式時列出其參數,然後傳遞引數呼叫函式。
在上面的範例中,有人可能會說:「函式 showMessage
宣告時有兩個參數,然後傳遞兩個引數呼叫:from
和 "Hello"
」。
預設值
如果呼叫函式,但未提供引數,則對應值會變成 undefined
。
例如,前面提到的函式 showMessage(from, text)
可以傳遞一個引數呼叫
showMessage("Ann");
這不是錯誤。這樣的呼叫會輸出 "*Ann*: undefined"
。由於未傳遞 text
的值,因此會變成 undefined
。
我們可以在函式宣告中使用 =
指定參數的所謂「預設」(如果省略時使用)值
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}
showMessage("Ann"); // Ann: no text given
現在,如果未傳遞 text
參數,它將取得值 "no text given"
。
如果參數存在,但嚴格等於 undefined
,預設值也會介入,如下所示
showMessage("Ann", undefined); // Ann: no text given
這裡 "no text given"
是字串,但它可以是更複雜的表達式,只有在參數遺失時才會評估和指定。因此,這也是可能的
function showMessage(from, text = anotherFunction()) {
// anotherFunction() only executed if no text given
// its result becomes the value of text
}
在 JavaScript 中,預設參數會在每次在沒有對應參數的情況下呼叫函式時評估。
在上面的範例中,如果提供了 text
參數,則根本不會呼叫 anotherFunction()
。
另一方面,每次遺失 text
時,它會獨立呼叫。
幾年前,JavaScript 不支援預設參數的語法。因此,人們使用其他方式來指定它們。
現在,我們可以在舊腳本中看到它們。
例如,明確檢查 undefined
function showMessage(from, text) {
if (text === undefined) {
text = 'no text given';
}
alert( from + ": " + text );
}
…或使用 ||
算子
function showMessage(from, text) {
// If the value of text is falsy, assign the default value
// this assumes that text == "" is the same as no text at all
text = text || 'no text given';
...
}
替代預設參數
有時,在函式宣告之後的後續階段為參數指定預設值是有意義的。
我們可以透過將參數與 undefined
比較,在函式執行期間檢查是否傳遞了參數
function showMessage(text) {
// ...
if (text === undefined) { // if the parameter is missing
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message
…或我們可以使用 ||
算子
function showMessage(text) {
// if text is undefined or otherwise falsy, set it to 'empty'
text = text || 'empty';
...
}
現代 JavaScript 引擎支援 Null 合併運算子 ??
,當大多數假值(例如 0
)應被視為「正常」時,它會更好
function showCount(count) {
// if count is undefined or null, show "unknown"
alert(count ?? "unknown");
}
showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown
傳回值
函式可以將值傳回呼叫程式碼作為結果。
最簡單的範例會是將兩個值相加的函式
function sum(a, b) {
return a + b;
}
let result = sum(1, 2);
alert( result ); // 3
return
指令可以出現在函式的任何地方。當執行到達指令時,函式就會停止,而且值會傳回呼叫程式碼(指定給上方的 result
)。
單一函式中可能有多個 return
。例如
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('Do you have permission from your parents?');
}
}
let age = prompt('How old are you?', 18);
if ( checkAge(age) ) {
alert( 'Access granted' );
} else {
alert( 'Access denied' );
}
可以不帶值使用 return
。這會導致函式立即結束。
例如
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
alert( "Showing you the movie" ); // (*)
// ...
}
在上方程式碼中,如果 checkAge(age)
傳回 false
,則 showMovie
就不會繼續執行 alert
。
return
或沒有 return
的函式會傳回 undefined
如果函式沒有傳回值,就等於傳回 undefined
function doNothing() { /* empty */ }
alert( doNothing() === undefined ); // true
空 return
也等於 return undefined
function doNothing() {
return;
}
alert( doNothing() === undefined ); // true
return
和值之間加入換行符。對於 return
中的長式運算式,可能會想將其放在個別行中,如下所示
return
(some + long + expression + or + whatever * f(a) + f(b))
這無法運作,因為 JavaScript 會假設 return
之後有一個分號。這會與下列運作方式相同
return;
(some + long + expression + or + whatever * f(a) + f(b))
因此,它實際上會變成空回傳。
如果我們想要讓傳回的運算式跨多行,我們應該從與 return
相同的行開始。或者至少將開啟括號放在那裡,如下所示
return (
some + long + expression
+ or +
whatever * f(a) + f(b)
)
而且它會如我們預期般運作。
命名函式
函式是動作。因此它們的名稱通常是動詞。它應該簡潔、盡可能準確,而且描述函式的功能,以便閱讀程式碼的人可以了解函式的功能。
一個廣泛的作法是使用動詞前綴來開始函式,該前綴含糊地描述動作。團隊內部必須對前綴的意義達成共識。
例如,以 "show"
開頭的函式通常會顯示某些內容。
以…開頭的函式
"get…"
– 傳回值,"calc…"
– 計算某些內容,"create…"
– 建立某些內容,"check…"
– 檢查某些內容並傳回布林值,等等。
此類名稱的範例
showMessage(..) // shows a message
getAge(..) // returns the age (gets it somehow)
calcSum(..) // calculates a sum and returns the result
createForm(..) // creates a form (and usually returns it)
checkPermission(..) // checks a permission, returns true/false
有了前綴,只要看一眼函式名稱就能了解它執行哪種類型的任務,以及傳回哪種類型的值。
函數應僅執行其名稱所建議的動作,不應有其他動作。
兩個獨立的動作通常需要兩個函數,即使它們通常會一起呼叫(在這種情況下,我們可以建立一個呼叫這兩個函數的第三個函數)。
違反此規則的一些範例
getAge
– 如果它顯示一個包含年齡的alert
,則會很糟糕(它只應取得年齡)。createForm
– 如果它修改文件並向其中新增一個表單,則會很糟糕(它只應建立表單並傳回)。checkPermission
– 如果它顯示access granted/denied
訊息,則會很糟糕(它只應執行檢查並傳回結果)。
這些範例假設前綴詞有常見的意義。您和您的團隊可以自由同意其他意義,但通常它們不會有太大的不同。在任何情況下,您都應清楚了解前綴詞的意義,以及帶前綴的函數可以和不可以做什麼。所有具有相同前綴的函數都應遵守這些規則。而且團隊應共享這些知識。
函數 == 註解
函數應簡短且只做一件事。如果那件事很大,也許值得將函數拆分成幾個較小的函數。有時遵循此規則可能並不容易,但這絕對是一件好事。
一個獨立的函數不僅更容易測試和除錯,其存在本身就是一個很棒的註解!
例如,比較以下兩個函數 showPrimes(n)
。每個函數都輸出小於或等於 n
的質數。
第一個變體使用標籤
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert( i ); // a prime
}
}
第二個變體使用一個額外的函數 isPrime(n)
來測試質數
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // a prime
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
第二個變體更容易理解,不是嗎?我們看到的是動作的名稱(isPrime
),而不是程式碼片段。人們有時會將這種程式碼稱為自我描述。
因此,即使我們不打算重複使用函數,也可以建立函數。它們會建構程式碼並使其易於閱讀。
摘要
函數宣告看起來像這樣
function name(parameters, delimited, by, comma) {
/* code */
}
- 傳遞給函數作為參數的值會複製到其局部變數中。
- 函數可以存取外部變數。但它只能從內而外執行。函數外部的程式碼看不到其局部變數。
- 函數可以傳回一個值。如果沒有,則其結果為
undefined
。
為了使程式碼簡潔且易於理解,建議在函數中主要使用局部變數和參數,而不是外部變數。
理解一個取得參數、使用它們並傳回結果的函數,總是比一個不取得參數,但修改外部變數作為副作用的函數容易。
函數命名
- 名稱應清楚地描述函數的作用。當我們在程式碼中看到函數呼叫時,一個好的名稱會立即讓我們了解它做了什麼並傳回什麼。
- 函數是一種動作,因此函數名稱通常是動詞。
- 存在許多眾所周知的前置詞,例如
create…
、show…
、get…
、check…
等。使用它們來暗示函數的作用。
函數是腳本的主要建構區塊。現在我們已經涵蓋了基礎知識,因此我們實際上可以開始建立和使用它們。但這只是道路的開始。我們將多次回顧它們,深入探討它們的高階功能。
留言
<code>
標籤,如需插入多行,請將其包覆在<pre>
標籤中,如需插入超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)