在 JavaScript 中,函數不是“神奇的語言結構”,而是壹種特殊的值。
我們在前面章節使用的語法稱爲 函數聲明:
function sayHi() {
alert( "Hello" );
}另壹種創建函數的語法稱爲 函數表達式。
它允許我們在任何表達式的中間創建壹個新函數。
例如:
let sayHi = function() {
alert( "Hello" );
};在這裏我們可以看到變量 sayHi 得到了壹個值,新函數 function() { alert("Hello"); }。
由于函數創建發生在賦值表達式的上下文中(在 = 的右側),因此這是壹個 函數表達式。
請注意,function 關鍵字後面沒有函數名。函數表達式允許省略函數名。
這裏我們立即將它賦值給變量,所以上面的兩個代碼示例的含義是壹樣的:“創建壹個函數並將其放入變量 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() { // (1) 創建
alert( "Hello" );
};
let func = sayHi;
// ...這兩種聲明的函數是壹樣的。
爲什麽這裏末尾會有個分號?
妳可能想知道,爲什麽函數表達式結尾有壹個分號 ;,而函數聲明沒有:
function sayHi() {
// ...
}
let sayHi = function() {
// ...
};答案很簡單:這裏函數表達式是在賦值語句 let sayHi = ...; 中以 function(…) {…} 的形式創建的。建議在語句末尾加上分號 ;,它不是函數語法的壹部分。
分號用于更簡單的賦值,例如 let sayHi = 5;,它也用于函數賦值。
讓我們多舉幾個例子,看看如何將函數作爲值來傳遞以及如何使用函數表達式。
我們寫壹個包含三個參數的函數 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)。
我們可以在變量之間傳遞它們,並在需要時運行。
讓我們來總結壹下函數聲明和函數表達式之間的主要區別。
首先是語法:如何通過代碼對它們進行區分。
函數聲明:在主代碼流中聲明爲單獨的語句的函數:
// 函數聲明
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(…) {…} 更容易。函數聲明更“醒目”。
……但是,如果由于某種原因而導致函數聲明不適合我們(我們剛剛看過上面的例子),那麽應該使用函數表達式。
函數是值。它們可以在代碼的任何地方被分配,複制或聲明。
如果函數在主代碼流中被聲明爲單獨的語句,則稱爲“函數聲明”。
如果該函數是作爲表達式的壹部分創建的,則稱其“函數表達式”。
在執行代碼塊之前,內部算法會先處理函數聲明。所以函數聲明在其被聲明的代碼塊內的任何位置都是可見的。
函數表達式在執行流程到達時創建。
在大多數情況下,當我們需要聲明壹個函數時,最好使用函數聲明,因爲函數在被聲明之前也是可見的。這使我們在代碼組織方面更具靈活性,通常也會使得代碼可讀性更高。
所以,僅當函數聲明不適合對應的任務時,才應使用函數表達式。在本章中,我們已經看到了幾個例子,以後還會看到更多的例子。