2022 年 10 月 14 日

函數

我們常常需要在腳本的許多地方執行類似的動作。

例如,當訪客登入、登出,或是在其他地方時,我們需要顯示一則美觀的訊息。

函數是程式的主要「建構區塊」。它們讓程式碼可以在不重複的情況下被呼叫多次。

我們已經看過內建函數的範例,例如 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,稱為全域變數。

全域變數可從任何函數看到(除非被局部變數遮蔽)。

盡量減少使用全域變數是一種良好的習慣。現代程式碼很少或沒有全域變數。大多數變數都存在於其函數中。不過,有時它們可以用於儲存專案層級資料。

參數

我們可以使用參數將任意資料傳遞給函數。

在以下範例中,函數有兩個參數:fromtext

function showMessage(from, text) { // parameters: from, text
  alert(from + ': ' + text);
}

showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)

當函數在行 (*)(**) 中被呼叫時,給定的值會複製到局部變數 fromtext。然後函數使用它們。

以下是一個範例:我們有一個變數 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 程式碼中的預設參數

幾年前,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 訊息,則會很糟糕(它只應執行檢查並傳回結果)。

這些範例假設前綴詞有常見的意義。您和您的團隊可以自由同意其他意義,但通常它們不會有太大的不同。在任何情況下,您都應清楚了解前綴詞的意義,以及帶前綴的函數可以和不可以做什麼。所有具有相同前綴的函數都應遵守這些規則。而且團隊應共享這些知識。

超短函數名稱

非常頻繁 使用的函數有時會有超短的名稱。

例如,jQuery 框架定義了一個名稱為 $ 的函數。Lodash 函式庫的核心函數名稱為 _

這些是例外。一般來說,函數名稱應簡潔且具有描述性。

函數 == 註解

函數應簡短且只做一件事。如果那件事很大,也許值得將函數拆分成幾個較小的函數。有時遵循此規則可能並不容易,但這絕對是一件好事。

一個獨立的函數不僅更容易測試和除錯,其存在本身就是一個很棒的註解!

例如,比較以下兩個函數 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… 等。使用它們來暗示函數的作用。

函數是腳本的主要建構區塊。現在我們已經涵蓋了基礎知識,因此我們實際上可以開始建立和使用它們。但這只是道路的開始。我們將多次回顧它們,深入探討它們的高階功能。

任務

重要性:4

如果參數 age 大於 18,下列函數會傳回 true

否則,它會要求確認並傳回其結果

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    // ...
    return confirm('Did parents allow you?');
  }
}

如果移除 else,函數是否會以不同的方式運作?

function checkAge(age) {
  if (age > 18) {
    return true;
  }
  // ...
  return confirm('Did parents allow you?');
}

這兩個變體的行為是否有任何差異?

沒有差異!

在兩種情況下,return confirm('Did parents allow you?') 都會在 if 條件為假時執行。

重要性:4

如果參數 age 大於 18,下列函數會傳回 true

否則,它會要求確認並傳回其結果。

function checkAge(age) {
  if (age > 18) {
    return true;
  } else {
    return confirm('Did parents allow you?');
  }
}

改寫它,以執行相同的操作,但不用 if,在一行中。

建立 checkAge 的兩個變體

  1. 使用問號運算子 ?
  2. 使用 OR ||

使用問號運算子 '?'

function checkAge(age) {
  return (age > 18) ? true : confirm('Did parents allow you?');
}

使用 OR ||(最短的變體)

function checkAge(age) {
  return (age > 18) || confirm('Did parents allow you?');
}

請注意,這裡不需要 age > 18 周圍的括號。它們的存在是為了提高可讀性。

重要性:1

撰寫函數 min(a,b),傳回兩個數字 ab 中的最小值。

例如

min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1

使用 if 的解法

function min(a, b) {
  if (a < b) {
    return a;
  } else {
    return b;
  }
}

使用問號運算子 '?' 的解法

function min(a, b) {
  return a < b ? a : b;
}

附註:在相等的情況下 a == b,傳回什麼並不重要。

重要性:4

撰寫函數 pow(x,n),傳回 xn 次方。或者,換句話說,將 x 乘以本身 n 次並傳回結果。

pow(3, 2) = 3 * 3 = 9
pow(3, 3) = 3 * 3 * 3 = 27
pow(1, 100) = 1 * 1 * ...* 1 = 1

建立一個網頁,提示輸入 xn,然後顯示 pow(x,n) 的結果。

執行示範

附註:在此任務中,函數應僅支援 n 的自然數值:從 1 開始的整數。

function pow(x, n) {
  let result = x;

  for (let i = 1; i < n; i++) {
    result *= x;
  }

  return result;
}

let x = prompt("x?", '');
let n = prompt("n?", '');

if (n < 1) {
  alert(`Power ${n} is not supported, use a positive integer`);
} else {
  alert( pow(x, n) );
}
教學課程地圖

留言

留言前請先閱讀此內容…
  • 如果您有改進建議,請 提交 GitHub 問題 或提出拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 如需插入幾行程式碼,請使用 <code> 標籤,如需插入多行,請將其包覆在 <pre> 標籤中,如需插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)