在 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(…) {…}
更容易。函數聲明更“醒目”。
……但是,如果由于某種原因而導致函數聲明不適合我們(我們剛剛看過上面的例子),那麽應該使用函數表達式。
函數是值。它們可以在代碼的任何地方被分配,複制或聲明。
如果函數在主代碼流中被聲明爲單獨的語句,則稱爲“函數聲明”。
如果該函數是作爲表達式的壹部分創建的,則稱其“函數表達式”。
在執行代碼塊之前,內部算法會先處理函數聲明。所以函數聲明在其被聲明的代碼塊內的任何位置都是可見的。
函數表達式在執行流程到達時創建。
在大多數情況下,當我們需要聲明壹個函數時,最好使用函數聲明,因爲函數在被聲明之前也是可見的。這使我們在代碼組織方面更具靈活性,通常也會使得代碼可讀性更高。
所以,僅當函數聲明不適合對應的任務時,才應使用函數表達式。在本章中,我們已經看到了幾個例子,以後還會看到更多的例子。