Javascript 函數(shù)表達式

2023-02-17 10:37 更新

在 JavaScript 中,函數(shù)不是“神奇的語言結構”,而是一種特殊的值。

我們在前面章節(jié)使用的語法稱為 函數(shù)聲明

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

另一種創(chuàng)建函數(shù)的語法稱為 函數(shù)表達式。

它允許我們在任何表達式的中間創(chuàng)建一個新函數(shù)。

例如:

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

在這里我們可以看到變量 sayHi 得到了一個值,新函數(shù) function() { alert("Hello"); }。

由于函數(shù)創(chuàng)建發(fā)生在賦值表達式的上下文中(在 = 的右側(cè)),因此這是一個 函數(shù)表達式。

請注意,function 關鍵字后面沒有函數(shù)名。函數(shù)表達式允許省略函數(shù)名。

這里我們立即將它賦值給變量,所以上面的兩個代碼示例的含義是一樣的:“創(chuàng)建一個函數(shù)并將其放入變量 sayHi 中”。

在更多更高階的情況下,稍后我們會遇到,可以創(chuàng)建一個函數(shù)并立即調(diào)用,或者安排稍后執(zhí)行,而不是存儲在任何地方,因此保持匿名。

函數(shù)是一個值

重申一次:無論函數(shù)是如何創(chuàng)建的,函數(shù)都是一個值。上面的兩個示例都在 sayHi 變量中存儲了一個函數(shù)。

我們還可以用 alert 顯示這個變量的值:

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

alert( sayHi ); // 顯示函數(shù)代碼

注意,最后一行代碼并不會運行函數(shù),因為 sayHi 后沒有括號。在某些編程語言中,只要提到函數(shù)的名稱都會導致函數(shù)的調(diào)用執(zhí)行,但 JavaScript 可不是這樣。

在 JavaScript 中,函數(shù)是一個值,所以我們可以把它當成值對待。上面代碼顯示了一段字符串值,即函數(shù)的源碼。

的確,在某種意義上說一個函數(shù)是一個特殊值,我們可以像 sayHi() 這樣調(diào)用它。

但它依然是一個值,所以我們可以像使用其他類型的值一樣使用它。

我們可以復制函數(shù)到其他變量:

function sayHi() {   // (1) 創(chuàng)建
  alert( "Hello" );
}

let func = sayHi;    // (2) 復制

func(); // Hello     // (3) 運行復制的值(正常運行)!
sayHi(); // Hello    //     這里也能運行(為什么不行呢)

解釋一下上段代碼發(fā)生的細節(jié):

  1. ?(1)? 行聲明創(chuàng)建了函數(shù),并把它放入到變量 ?sayHi?。
  2. ?(2)? 行將 ?sayHi ?復制到了變量 ?func?。請注意:?sayHi ?后面沒有括號。如果有括號,?func = sayHi()? 會把 ?sayHi()? 的調(diào)用結果寫進?func?,而不是 ?sayHi ?函數(shù) 本身。
  3. 現(xiàn)在函數(shù)可以通過 ?sayHi()? 和 ?func()? 兩種方式進行調(diào)用。

我們也可以在第一行中使用函數(shù)表達式來聲明 ?sayHi?:

let sayHi = function() { // (1) 創(chuàng)建
  alert( "Hello" );
};

let func = sayHi;
// ...

這兩種聲明的函數(shù)是一樣的。

為什么這里末尾會有個分號?

你可能想知道,為什么函數(shù)表達式結尾有一個分號 ;,而函數(shù)聲明沒有:

function sayHi() {
  // ...
}

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

答案很簡單:這里函數(shù)表達式是在賦值語句 let sayHi = ...; 中以 function(…) {…} 的形式創(chuàng)建的。建議在語句末尾加上分號 ;,它不是函數(shù)語法的一部分。

分號用于更簡單的賦值,例如 let sayHi = 5;,它也用于函數(shù)賦值。

回調(diào)函數(shù)

讓我們多舉幾個例子,看看如何將函數(shù)作為值來傳遞以及如何使用函數(shù)表達式。

我們寫一個包含三個參數(shù)的函數(shù) ask(question, yes, no)

?question
?

關于問題的文本

?yes
?

當回答為 “Yes” 時,要運行的腳本

?no
?

當回答為 “No” 時,要運行的腳本

函數(shù)需要提出 question(問題),并根據(jù)用戶的回答,調(diào)用 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." );
}

// 用法:函數(shù) showOk 和 showCancel 被作為參數(shù)傳入到 ask
ask("Do you agree?", showOk, showCancel);

在實際開發(fā)中,這樣的函數(shù)是非常有用的。實際開發(fā)與上述示例最大的區(qū)別是,實際開發(fā)中的函數(shù)會通過更加復雜的方式與用戶進行交互,而不是通過簡單的 confirm。在瀏覽器中,這樣的函數(shù)通常會繪制一個漂亮的提問窗口。但這是另外一件事了。

ask 的兩個參數(shù)值 showOk 和 showCancel 可以被稱為 回調(diào)函數(shù) 或簡稱 回調(diào)

主要思想是我們傳遞一個函數(shù),并期望在稍后必要時將其“回調(diào)”。在我們的例子中,showOk 是回答 “yes” 的回調(diào),showCancel 是回答 “no” 的回調(diào)。

我們可以使用函數(shù)表達式來編寫一個等價的、更簡潔的函數(shù):

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(...) 調(diào)用內(nèi)進行函數(shù)聲明。這兩個函數(shù)沒有名字,所以叫 匿名函數(shù)。這樣的函數(shù)在 ask 外無法訪問(因為沒有對它們分配變量),不過這正是我們想要的。

這樣的代碼在我們的腳本中非常常見,這正符合 JavaScript 語言的思想。

一個函數(shù)是表示一個“行為”的值

字符串或數(shù)字等常規(guī)值代表 數(shù)據(jù)。

函數(shù)可以被視為一個 行為(action)

我們可以在變量之間傳遞它們,并在需要時運行。

函數(shù)表達式 vs 函數(shù)聲明

讓我們來總結一下函數(shù)聲明和函數(shù)表達式之間的主要區(qū)別。

首先是語法:如何通過代碼對它們進行區(qū)分。

  • 函數(shù)聲明:在主代碼流中聲明為單獨的語句的函數(shù):
  • // 函數(shù)聲明
    function sum(a, b) {
      return a + b;
    }
  • 函數(shù)表達式:在一個表達式中或另一個語法結構中創(chuàng)建的函數(shù)。下面這個函數(shù)是在賦值表達式 = 右側(cè)創(chuàng)建的:
  • // 函數(shù)表達式
    let sum = function(a, b) {
      return a + b;
    };

更細微的差別是,JavaScript 引擎會在 什么時候 創(chuàng)建函數(shù)。

函數(shù)表達式是在代碼執(zhí)行到達時被創(chuàng)建,并且僅從那一刻起可用。

一旦代碼執(zhí)行到賦值表達式 let sum = function… 的右側(cè),此時就會開始創(chuàng)建該函數(shù),并且可以從現(xiàn)在開始使用(分配,調(diào)用等)。

函數(shù)聲明則不同。

在函數(shù)聲明被定義之前,它就可以被調(diào)用。

例如,一個全局函數(shù)聲明對整個腳本來說都是可見的,無論它被寫在這個腳本的哪個位置。

這是內(nèi)部算法的原故。當 JavaScript 準備 運行腳本時,首先會在腳本中尋找全局函數(shù)聲明,并創(chuàng)建這些函數(shù)。我們可以將其視為“初始化階段”。

在處理完所有函數(shù)聲明后,代碼才被執(zhí)行。所以運行時能夠使用這些函數(shù)。

例如下面的代碼會正常工作:

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

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

函數(shù)聲明 sayHi 是在 JavaScript 準備運行腳本時被創(chuàng)建的,在這個腳本的任何位置都可見。

……如果它是一個函數(shù)表達式,它就不會工作:

sayHi("John"); // error!

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

函數(shù)表達式在代碼執(zhí)行到它時才會被創(chuàng)建。只會發(fā)生在 (*) 行。為時已晚。

函數(shù)聲明的另外一個特殊的功能是它們的塊級作用域。

嚴格模式下,當一個函數(shù)聲明在一個代碼塊內(nèi)時,它在該代碼塊內(nèi)的任何位置都是可見的。但在代碼塊外不可見。

例如,想象一下我們需要依賴于在代碼運行過程中獲得的變量 age 聲明一個函數(shù) welcome()。并且我們計劃在之后的某個時間使用它。

如果我們使用函數(shù)聲明,則以下代碼無法像預期那樣工作:

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

// 有條件地聲明一個函數(shù)
if (age < 18) {

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

} else {

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

}

// ……稍后使用
welcome(); // Error: welcome is not defined

這是因為函數(shù)聲明只在它所在的代碼塊中可見。

下面是另一個例子:

let age = 16; // 拿 16 作為例子

if (age < 18) {
  welcome();               // \   (運行)
                           //  |
  function welcome() {     //  |
    alert("Hello!");       //  |  函數(shù)聲明在聲明它的代碼塊內(nèi)任意位置都可用
  }                        //  |
                           //  |
  welcome();               // /   (運行)

} else {

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

// 在這里,我們在花括號外部調(diào)用函數(shù),我們看不到它們內(nèi)部的函數(shù)聲明。


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

我們怎么才能讓 welcome 在 if 外可見呢?

正確的做法是使用函數(shù)表達式,并將 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(); // 現(xiàn)在可以了

或者我們可以使用問號運算符 ? 來進一步對代碼進行簡化:

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

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

welcome(); // 現(xiàn)在可以了

什么時候選擇函數(shù)聲明與函數(shù)表達式?

根據(jù)經(jīng)驗,當我們需要聲明一個函數(shù)時,首先考慮函數(shù)聲明語法。它能夠為組織代碼提供更多的靈活性。因為我們可以在聲明這些函數(shù)之前調(diào)用這些函數(shù)。

這對代碼可讀性也更好,因為在代碼中查找 function f(…) {…} 比 let f = function(…) {…} 更容易。函數(shù)聲明更“醒目”。

……但是,如果由于某種原因而導致函數(shù)聲明不適合我們(我們剛剛看過上面的例子),那么應該使用函數(shù)表達式。

總結

  • 函數(shù)是值。它們可以在代碼的任何地方被分配,復制或聲明。
  • 如果函數(shù)在主代碼流中被聲明為單獨的語句,則稱為“函數(shù)聲明”。
  • 如果該函數(shù)是作為表達式的一部分創(chuàng)建的,則稱其“函數(shù)表達式”。
  • 在執(zhí)行代碼塊之前,內(nèi)部算法會先處理函數(shù)聲明。所以函數(shù)聲明在其被聲明的代碼塊內(nèi)的任何位置都是可見的。
  • 函數(shù)表達式在執(zhí)行流程到達時創(chuàng)建。

在大多數(shù)情況下,當我們需要聲明一個函數(shù)時,最好使用函數(shù)聲明,因為函數(shù)在被聲明之前也是可見的。這使我們在代碼組織方面更具靈活性,通常也會使得代碼可讀性更高。

所以,僅當函數(shù)聲明不適合對應的任務時,才應使用函數(shù)表達式。在本章中,我們已經(jīng)看到了幾個例子,以后還會看到更多的例子。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號