JavaScript 基礎知識6 函式表示式
JavaScript 基礎知識6 函式表示式
我們在前面章節使用的語法稱為 函式宣告:
function sayHi() {
alert( "Hello" );
}
另一種建立函式的語法稱為 函式表示式。
通常會寫成這樣:
let sayHi = function() {
alert( "Hello" );
};
在這裡,函式被建立並像其他賦值一樣,被明確地分配給了一個變數。不管函式是被怎樣定義的,都只是一個儲存在變數 sayHi 中的值。
上面這兩段示例程式碼的意思是一樣的:“建立一個函式,並把它存進變數 sayHi”。
我們還可以用 alert 列印這個變數值:
function sayHi() {
alert( "Hello" );
}
alert( sayHi ); // 顯示函式程式碼
注意,最後一行程式碼並不會執行函式,因為 sayHi 後沒有括號。在某些程式語言中,只要提到函式的名稱都會導致函式的呼叫執行,但 JavaScript 可不是這樣。
在 JavaScript 中,函式是一個值,所以我們可以把它當成值對待。上面程式碼顯示了一段字串值,即函式的原始碼。
的確,在某種意義上說一個函式是一個特殊值,我們可以像 sayHi() 這樣呼叫它。
但它依然是一個值,所以我們可以像使用其他型別的值一樣使用它。
我們可以複製函式到其他變數:
function sayHi() { // (1) 建立
alert( "Hello" );
}
let func = sayHi; // (2) 複製
func(); // Hello // (3) 運行復制的值(正常執行)!
sayHi(); // Hello // 這裡也能執行(為什麼不行呢)
解釋一下上段程式碼發生的細節:
(1) 行宣告建立了函式,並把它放入到變數 sayHi。
(2) 行將 sayHi 複製到了變數 func。請注意:sayHi 後面沒有括號。如果有括號,func = sayHi() 會把 sayHi() 的呼叫結果寫進func,而不是 sayHi 函式 本身。
現在函式可以通過 sayHi() 和 func() 兩種方式進行呼叫。
注意,我們也可以在第一行中使用函式表示式來宣告 sayHi:
let sayHi = function() {
alert( "Hello" );
};
let func = sayHi;
// ...
這兩種宣告的函式是一樣的。
為什麼這裡末尾會有個分號?
你可能想知道,為什麼函式表示式結尾有一個分號 ;,而函式宣告沒有:
function sayHi() {
// ...
}
let sayHi = function() {
// ...
};
答案很簡單:
在程式碼塊的結尾不需要加分號 ;,像 if { ... },for { },function f { } 等語法結構後面都不用加。
函式表示式是在語句內部的:let sayHi = ...;,作為一個值。它不是程式碼塊而是一個賦值語句。不管值是什麼,都建議在語句末尾新增分號 ;。所以這裡的分號與函式表示式本身沒有任何關係,它只是用於終止語句。
回撥函式
讓我們多舉幾個例子,看看如何將函式作為值來傳遞以及如何使用函式表示式。
我們寫一個包含三個引數的函式 ask(question, yes, no):
question
關於問題的文字
yes
當回答為 “Yes” 時,要執行的指令碼
no
當回答為 “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." );
}
// 用法:函式 showOk 和 showCancel 被作為引數傳入到 ask
ask("Do you agree?", showOk, showCancel);
在實際開發中,這樣的函式是非常有用的。實際開發與上述示例最大的區別是,實際開發中的函式會通過更加複雜的方式與使用者進行互動,而不是通過簡單的 confirm。在瀏覽器中,這樣的函式通常會繪製一個漂亮的提問視窗。但這是另外一件事了。
ask 的兩個引數值 showOk 和 showCancel 可以被稱為 回撥函式 或簡稱 回撥。
主要思想是我們傳遞一個函式,並期望在稍後必要時將其“回撥”。在我們的例子中,showOk 是回答 “yes” 的回撥,showCancel 是回答 “no” 的回撥。
我們可以用函式表示式對同樣的函式進行大幅簡寫:
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 語言的思想。
一個函式是表示一個“行為”的值
字串或數字等常規值代表 資料。
函式可以被視為一個 行為(action)。
我們可以在變數之間傳遞它們,並在需要時執行。
函式表示式 vs 函式宣告
讓我們來總結一下函式宣告和函式表示式之間的主要區別。
首先是語法:如何通過程式碼對它們進行區分。
函式宣告:在主程式碼流中宣告為單獨的語句的函式。
// 函式宣告
function sum(a, b) {
return a + b;
}
函式表示式:在一個表示式中或另一個語法結構中建立的函式。下面這個函式是在賦值表示式 = 右側建立的:
// 函式表示式
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}` );
};
函式表示式在程式碼執行到它時才會被建立。只會發生在 (*) 行。為時已晚。
函式宣告的另外一個特殊的功能是它們的塊級作用域。
嚴格模式下,當一個函式宣告在一個程式碼塊內時,它在該程式碼塊內的任何位置都是可見的。但在程式碼塊外不可見。
例如,想象一下我們需要依賴於在程式碼執行過程中獲得的變數 age 宣告一個函式 welcome()。並且我們計劃在之後的某個時間使用它。
如果我們使用函式宣告,則以下程式碼無法像預期那樣工作:
let age = prompt("What is your age?", 18);
// 有條件地宣告一個函式
if (age < 18) {
function welcome() {
alert("Hello!");
}
} else {
function welcome() {
alert("Greetings!");
}
}
// ……稍後使用
welcome(); // Error: welcome is not defined
這是因為函式宣告只在它所在的程式碼塊中可見。
下面是另一個例子:
let age = 16; // 拿 16 作為例子
if (age < 18) {
welcome(); // \ (執行)
// |
function welcome() { // |
alert("Hello!"); // | 函式宣告在宣告它的程式碼塊內任意位置都可用
} // |
// |
welcome(); // / (執行)
} else {
function welcome() {
alert("Greetings!");
}
}
// 在這裡,我們在花括號外部呼叫函式,我們看不到它們內部的函式宣告。
welcome(); // Error: welcome is not defined
我們怎麼才能讓 welcome 在 if 外可見呢?
正確的做法是使用函式表示式,並將 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(); // 現在可以了
或者我們可以使用問號運算子 ? 來進一步對程式碼進行簡化:
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
function() { alert("Hello!"); } :
function() { alert("Greetings!"); };
welcome(); // 現在可以了
什麼時候選擇函式宣告與函式表示式?
根據經驗,當我們需要宣告一個函式時,首先考慮函式宣告語法。它能夠為組織程式碼提供更多的靈活性。因為我們可以在宣告這些函式之前呼叫這些函式。
這對程式碼可讀性也更好,因為在程式碼中查詢 function f(…) {…} 比 let f = function(…) {…} 更容易。函式宣告更“醒目”。
……但是,如果由於某種原因而導致函式宣告不適合我們(我們剛剛看過上面的例子),那麼應該使用函式表示式。
總結
函式是值。它們可以在程式碼的任何地方被分配,複製或宣告。
如果函式在主程式碼流中被宣告為單獨的語句,則稱為“函式宣告”。
如果該函式是作為表示式的一部分建立的,則稱其“函式表示式”。
在執行程式碼塊之前,內部演算法會先處理函式宣告。所以函式宣告在其被宣告的程式碼塊內的任何位置都是可見的。
函式表示式在執行流程到達時建立。
在大多數情況下,當我們需要宣告一個函式時,最好使用函式宣告,因為函式在被宣告之前也是可見的。這使我們在程式碼組織方面更具靈活性,通常也會使得程式碼可讀性更高。
所以,僅當函式宣告不適合對應的任務時,才應使用函式表示式。在本章中,我們已經看到了幾個例子,以後還會看到更多的例子。