JavaScript 基礎知識6 函數表達式
我們在前面章節使用的文法稱為 函數聲明:
function sayHi() {
alert( "Hello" );
}
另一種建立函數的文法稱為 函數表達式。
通常會寫成這樣:
let sayHi = function() {
};
在這裡,函數被建立并像其他指派一樣,被明确地配置設定給了一個變量。不管函數是被怎樣定義的,都隻是一個存儲在變量 sayHi 中的值。
上面這兩段示例代碼的意思是一樣的:“建立一個函數,并把它存進變量 sayHi”。
我們還可以用 alert 列印這個變量值:
alert( sayHi ); // 顯示函數代碼
注意,最後一行代碼并不會運作函數,因為 sayHi 後沒有括号。在某些程式設計語言中,隻要提到函數的名稱都會導緻函數的調用執行,但 JavaScript 可不是這樣。
在 JavaScript 中,函數是一個值,是以我們可以把它當成值對待。上面代碼顯示了一段字元串值,即函數的源碼。
的确,在某種意義上說一個函數是一個特殊值,我們可以像 sayHi() 這樣調用它。
但它依然是一個值,是以我們可以像使用其他類型的值一樣使用它。
我們可以複制函數到其他變量:
function sayHi() { // (1) 建立
let func = sayHi; // (2) 複制
func(); // Hello // (3) 運作複制的值(正常運作)!
sayHi(); // Hello // 這裡也能運作(為什麼不行呢)
解釋一下上段代碼發生的細節:
(1) 行聲明建立了函數,并把它放入到變量 sayHi。
(2) 行将 sayHi 複制到了變量 func。請注意:sayHi 後面沒有括号。如果有括号,func = sayHi() 會把 sayHi() 的調用結果寫進func,而不是 sayHi 函數 本身。
現在函數可以通過 sayHi() 和 func() 兩種方式進行調用。
注意,我們也可以在第一行中使用函數表達式來聲明 sayHi:
let func = sayHi;
// ...
這兩種聲明的函數是一樣的。
為什麼這裡末尾會有個分号?
你可能想知道,為什麼函數表達式結尾有一個分号 ;,而函數聲明沒有:
// ...
答案很簡單:
在代碼塊的結尾不需要加分号 ;,像 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” 的回調。
我們可以用函數表達式對同樣的函數進行大幅簡寫:
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) {
更細微的差别是,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
函數表達式在代碼執行到它時才會被建立。隻會發生在 (*) 行。為時已晚。
函數聲明的另外一個特殊的功能是它們的塊級作用域。
嚴格模式下,當一個函數聲明在一個代碼塊内時,它在該代碼塊内的任何位置都是可見的。但在代碼塊外不可見。
例如,想象一下我們需要依賴于在代碼運作過程中獲得的變量 age 聲明一個函數 welcome()。并且我們計劃在之後的某個時間使用它。
如果我們使用函數聲明,則以下代碼無法像預期那樣工作:
let age = prompt("What is your age?", 18);
// 有條件地聲明一個函數
if (age < 18) {
function welcome() {
alert("Hello!");
}
} else {
alert("Greetings!");
// ……稍後使用
welcome(); // Error: welcome is not defined
這是因為函數聲明隻在它所在的代碼塊中可見。
下面是另一個例子:
let age = 16; // 拿 16 作為例子
welcome(); // \ (運作)
// |
function welcome() { // |
alert("Hello!"); // | 函數聲明在聲明它的代碼塊内任意位置都可用
} // |
welcome(); // / (運作)
// 在這裡,我們在花括号外部調用函數,我們看不到它們内部的函數聲明。
我們怎麼才能讓 welcome 在 if 外可見呢?
正确的做法是使用函數表達式,并将 welcome 指派給在 if 外聲明的變量,并具有正确的可見性。
下面的代碼可以如願運作:
let welcome;
welcome = function() {
};
welcome(); // 現在可以了
或者我們可以使用問号運算符 ? 來進一步對代碼進行簡化:
let welcome = (age < 18) ?
function() { alert("Hello!"); } :
function() { alert("Greetings!"); };
什麼時候選擇函數聲明與函數表達式?
根據經驗,當我們需要聲明一個函數時,首先考慮函數聲明文法。它能夠為組織代碼提供更多的靈活性。因為我們可以在聲明這些函數之前調用這些函數。
這對代碼可讀性也更好,因為在代碼中查找 function f(…) {…} 比 let f = function(…) {…} 更容易。函數聲明更“醒目”。