2022 年 7 月 14 日

函數表達式

在 JavaScript 中,函數並非「神奇的語言結構」,而是一種特殊類型的值。

我們之前使用的語法稱為函數宣告

function sayHi() {
  alert( "Hello" );
}

建立函數的另一種語法稱為函數表達式

它允許我們在任何表達式的中間建立一個新的函式。

例如

let sayHi = function() {
  alert( "Hello" );
};

在這裡,我們可以看到一個變數 sayHi 取得一個值,新的函式,建立為 function() { alert("Hello"); }

由於函式建立發生在指定表達式的內容中(在 = 的右側),這是一個函式表達式

請注意,function 關鍵字後面沒有名稱。對於函式表達式,允許省略名稱。

在這裡,我們立即將它指定給變數,因此這些程式碼範例的意思相同:「建立一個函式並將它放入變數 sayHi 中」。

在我們稍後會遇到的更進階的情況中,函式可能會建立並立即呼叫或排程為稍後執行,不會儲存在任何地方,因此保持匿名。

函式是一個值

讓我們重申:不論函式如何建立,函式都是一個值。以上兩個範例都將函式儲存在 sayHi 變數中。

我們甚至可以使用 alert 印出該值

function sayHi() {
  alert( "Hello" );
}

alert( sayHi ); // shows the function code

請注意,最後一行不會執行函式,因為 sayHi 後面沒有括號。有些程式語言中,任何提到函式名稱都會導致其執行,但 JavaScript 並非如此。

在 JavaScript 中,函式是一個值,因此我們可以將其視為一個值來處理。以上的程式碼顯示了它的字串表示,也就是原始碼。

當然,函式是一個特殊的值,因為我們可以像 sayHi() 一樣呼叫它。

但它仍然是一個值。因此,我們可以像處理其他類型的值一樣處理它。

我們可以將函式複製到另一個變數

function sayHi() {   // (1) create
  alert( "Hello" );
}

let func = sayHi;    // (2) copy

func(); // Hello     // (3) run the copy (it works)!
sayHi(); // Hello    //     this still works too (why wouldn't it)

以下是上面詳細發生的情況

  1. 函式宣告 (1) 建立函式並將它放入名為 sayHi 的變數中。
  2. (2) 行將它複製到變數 func 中。請再次注意:sayHi 後面沒有括號。如果有,則 func = sayHi() 會將呼叫的結果 sayHi() 寫入 func 中,而不是函式 sayHi 本身。
  3. 現在,函式可以同時作為 sayHi()func() 呼叫。

我們也可以使用函式表達式在第一行宣告 sayHi

let sayHi = function() { // (1) create
  alert( "Hello" );
};

let func = sayHi;
// ...

所有內容都會以相同的方式運作。

為什麼結尾處有一個分號?

你可能會想,為什麼函式表達式結尾處有一個分號 ;,但函式宣告沒有

function sayHi() {
  // ...
}

let sayHi = function() {
  // ...
};

答案很簡單:在指派陳述式中,建立一個函式表達式為 function(…) {…}let sayHi = …;。建議在陳述式的結尾加上分號 ;,它並非函式語法的部分。

在較簡單的指派中,例如 let sayHi = 5;,分號會出現在那裡,在函式指派中也是如此。

回呼函式

讓我們看看更多傳遞函式作為值和使用函式表達式的範例。

我們將撰寫一個函式 ask(question, yes, no),有三個參數

question
問題文字
yes
如果答案為「是」時執行的函式
no
如果答案為「否」時執行的函式

函式應詢問 question,並根據使用者的答案呼叫 yes()no()

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

function showOk() {
  alert( "You agreed." );
}

function showCancel() {
  alert( "You canceled the execution." );
}

// usage: functions showOk, showCancel are passed as arguments to ask
ask("Do you agree?", showOk, showCancel);

在實務上,此類函式非常有用。真實的 ask 與上述範例之間的主要差異在於,真實函式使用比簡單的 confirm 更複雜的方式與使用者互動。在瀏覽器中,此類函式通常會繪製一個美觀的問題視窗。但那是另一個故事了。

ask 的引數 showOkshowCancel 稱為回呼函式或僅稱為回呼

這個想法是我們傳遞一個函式,並期望它在必要時「回呼」。在我們的案例中,showOk 成為「是」答案的回呼,而 showCancel 成為「否」答案的回呼。

我們可以使用函式表達式撰寫等效的較短函式

function ask(question, yes, no) {
  if (confirm(question)) yes()
  else no();
}

ask(
  "Do you agree?",
  function() { alert("You agreed."); },
  function() { alert("You canceled the execution."); }
);

在此,函式直接在 ask(...) 呼叫中宣告。它們沒有名稱,因此稱為匿名。此類函式無法在 ask 外部存取(因為它們未指派給變數),但這正是我們在此想要的。

此類程式碼非常自然地出現在我們的腳本中,符合 JavaScript 的精神。

函式是一個表示「動作」的值

字串或數字等常規值表示資料

函式可以視為動作

我們可以在變數之間傳遞它,並在我們想要時執行它。

函數表達式與函數宣告

讓我們整理函數宣告和表達式之間的主要差異。

首先,語法:如何區分程式碼中的兩者。

  • 函數宣告:函數,宣告為獨立陳述式,在主程式碼流程中

    // Function Declaration
    function sum(a, b) {
      return a + b;
    }
  • 函數表達式:函數,建立在表達式內或其他語法結構內。在此,函數建立在「賦值表達式」= 的右側

    // Function Expression
    let sum = function(a, b) {
      return a + b;
    };

更細微的差異是函數由 JavaScript 引擎建立的時間點

函數表達式在執行到達它時建立,且僅從那一刻起可以使用。

一旦執行流程傳遞到賦值的右側 let sum = function… – 我們開始吧,函數建立了,從現在開始可以使用(賦值、呼叫等)。

函數宣告不同。

函數宣告可以在定義之前呼叫。

例如,全域函數宣告在整個腳本中都可見,無論它在哪裡。

這是由於內部演算法。當 JavaScript 準備執行腳本時,它會先在其中尋找全域函數宣告,並建立函數。我們可以將其視為「初始化階段」。

在所有函數宣告都處理完畢後,程式碼才會執行。因此,它可以存取這些函數。

例如,這是可行的

sayHi("John"); // Hello, John

function sayHi(name) {
  alert( `Hello, ${name}` );
}

函數宣告 sayHi 在 JavaScript 準備開始腳本時建立,並在腳本中的任何地方都可見。

…如果它是函數表達式,那麼它將無法執行

sayHi("John"); // error!

let sayHi = function(name) {  // (*) no magic any more
  alert( `Hello, ${name}` );
};

函數表達式在執行到達它們時建立。這只會在第 (*) 行發生。太遲了。

函數宣告的另一個特殊功能是它們的區塊範圍。

在嚴格模式中,當函數宣告在程式碼區塊內時,它在該區塊內的任何地方都可見。但區塊外不可見。

例如,讓我們想像我們需要宣告一個函數 welcome(),它取決於我們在執行期間取得的 age 變數。然後我們計畫稍後使用它。

如果我們使用函數宣告,它將無法按預期執行

let age = prompt("What is your age?", 18);

// conditionally declare a function
if (age < 18) {

  function welcome() {
    alert("Hello!");
  }

} else {

  function welcome() {
    alert("Greetings!");
  }

}

// ...use it later
welcome(); // Error: welcome is not defined

這是因為函數宣告僅在其所在的程式碼區塊內可見。

以下是另一個範例

let age = 16; // take 16 as an example

if (age < 18) {
  welcome();               // \   (runs)
                           //  |
  function welcome() {     //  |
    alert("Hello!");       //  |  Function Declaration is available
  }                        //  |  everywhere in the block where it's declared
                           //  |
  welcome();               // /   (runs)

} else {

  function welcome() {
    alert("Greetings!");
  }
}

// Here we're out of curly braces,
// so we can not see Function Declarations made inside of them.

welcome(); // Error: welcome is not defined

我們可以做什麼讓 welcomeif 外部可見?

正確的方法是使用函數表達式,並將 welcome 賦值給在 if 外部宣告且具有適當可見性的變數。

這段程式碼按預期執行

let age = prompt("What is your age?", 18);

let welcome;

if (age < 18) {

  welcome = function() {
    alert("Hello!");
  };

} else {

  welcome = function() {
    alert("Greetings!");
  };

}

welcome(); // ok now

或者我們可以使用問號運算子 ? 進一步簡化它

let age = prompt("What is your age?", 18);

let welcome = (age < 18) ?
  function() { alert("Hello!"); } :
  function() { alert("Greetings!"); };

welcome(); // ok now
什麼時候選擇函數宣告而非函數表達式?

原則上,當我們需要宣告函式時,第一個要考慮的是函式宣告語法。它在如何組織我們的程式碼方面提供了更大的自由度,因為我們可以在宣告函式之前呼叫它們。

這對於可讀性也更好,因為在程式碼中查詢 function f(…) {…}let f = function(…) {…}; 容易。函式宣告更「引人注目」。

…但是,如果函式宣告由於某種原因不適合我們,或者我們需要條件宣告(我們剛剛看過一個範例),則應該使用函式表達式。

摘要

  • 函式是值。它們可以在程式碼的任何地方指派、複製或宣告。
  • 如果函式在主程式碼流程中宣告為單獨的陳述式,則稱為「函式宣告」。
  • 如果函式建立為表達式的一部分,則稱為「函式表達式」。
  • 函式宣告在執行程式碼區塊之前處理。它們在區塊中的任何地方都可見。
  • 函式表達式在執行流程到達它們時建立。

在我們需要宣告函式的大多數情況下,函式宣告是較佳的選擇,因為它在宣告本身之前可見。這讓我們在程式碼組織方面有更大的彈性,而且通常更具可讀性。

因此,我們應該僅在函式宣告不適合任務時才使用函式表達式。我們在本章中已經看過幾個範例,未來還會看到更多。

教學課程地圖

留言

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