在 JavaScript 中,函數並非「神奇的語言結構」,而是一種特殊類型的值。
我們之前使用的語法稱為函數宣告
function sayHi() {
alert( "Hello" );
}
建立函數的另一種語法稱為函數表達式。
它允許我們在任何表達式的中間建立一個新的函式。
例如
let sayHi = function() {
alert( "Hello" );
};
在這裡,我們可以看到一個變數 sayHi
取得一個值,新的函式,建立為 function() { alert("Hello"); }
。
由於函式建立發生在指定表達式的內容中(在 =
的右側),這是一個函式表達式。
請注意,function
關鍵字後面沒有名稱。對於函式表達式,允許省略名稱。
在這裡,我們立即將它指定給變數,因此這些程式碼範例的意思相同:「建立一個函式並將它放入變數 sayHi
中」。
在我們稍後會遇到的更進階的情況中,函式可能會建立並立即呼叫或排程為稍後執行,不會儲存在任何地方,因此保持匿名。
函式是一個值
讓我們重申:不論函式如何建立,函式都是一個值。以上兩個範例都將函式儲存在 sayHi
變數中。
我們甚至可以使用 alert
印出該值
function sayHi() {
alert( "Hello" );
}
alert( sayHi ); // shows the function code
請注意,最後一行不會執行函式,因為 sayHi
後面沒有括號。有些程式語言中,任何提到函式名稱都會導致其執行,但 JavaScript 並非如此。
在 JavaScript 中,函式是一個值,因此我們可以將其視為一個值來處理。以上的程式碼顯示了它的字串表示,也就是原始碼。
當然,函式是一個特殊的值,因為我們可以像 sayHi()
一樣呼叫它。
但它仍然是一個值。因此,我們可以像處理其他類型的值一樣處理它。
我們可以將函式複製到另一個變數
function sayHi() { // (1) create
alert( "Hello" );
}
let func = sayHi; // (2) copy
func(); // Hello // (3) run the copy (it works)!
sayHi(); // Hello // this still works too (why wouldn't it)
以下是上面詳細發生的情況
- 函式宣告
(1)
建立函式並將它放入名為sayHi
的變數中。 - 第
(2)
行將它複製到變數func
中。請再次注意:sayHi
後面沒有括號。如果有,則func = sayHi()
會將呼叫的結果sayHi()
寫入func
中,而不是函式sayHi
本身。 - 現在,函式可以同時作為
sayHi()
和func()
呼叫。
我們也可以使用函式表達式在第一行宣告 sayHi
let sayHi = function() { // (1) create
alert( "Hello" );
};
let func = sayHi;
// ...
所有內容都會以相同的方式運作。
你可能會想,為什麼函式表達式結尾處有一個分號 ;
,但函式宣告沒有
function sayHi() {
// ...
}
let sayHi = function() {
// ...
};
答案很簡單:在指派陳述式中,建立一個函式表達式為 function(…) {…}
:let sayHi = …;
。建議在陳述式的結尾加上分號 ;
,它並非函式語法的部分。
在較簡單的指派中,例如 let sayHi = 5;
,分號會出現在那裡,在函式指派中也是如此。
回呼函式
讓我們看看更多傳遞函式作為值和使用函式表達式的範例。
我們將撰寫一個函式 ask(question, yes, no)
,有三個參數
question
- 問題文字
yes
- 如果答案為「是」時執行的函式
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." );
}
// usage: functions showOk, showCancel are passed as arguments to ask
ask("Do you agree?", showOk, showCancel);
在實務上,此類函式非常有用。真實的 ask
與上述範例之間的主要差異在於,真實函式使用比簡單的 confirm
更複雜的方式與使用者互動。在瀏覽器中,此類函式通常會繪製一個美觀的問題視窗。但那是另一個故事了。
ask
的引數 showOk
和 showCancel
稱為回呼函式或僅稱為回呼。
這個想法是我們傳遞一個函式,並期望它在必要時「回呼」。在我們的案例中,showOk
成為「是」答案的回呼,而 showCancel
成為「否」答案的回呼。
我們可以使用函式表達式撰寫等效的較短函式
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 的精神。
字串或數字等常規值表示資料。
函式可以視為動作。
我們可以在變數之間傳遞它,並在我們想要時執行它。
函數表達式與函數宣告
讓我們整理函數宣告和表達式之間的主要差異。
首先,語法:如何區分程式碼中的兩者。
-
函數宣告:函數,宣告為獨立陳述式,在主程式碼流程中
// Function Declaration function sum(a, b) { return a + b; }
-
函數表達式:函數,建立在表達式內或其他語法結構內。在此,函數建立在「賦值表達式」
=
的右側// Function Expression 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}` );
};
函數表達式在執行到達它們時建立。這只會在第 (*)
行發生。太遲了。
函數宣告的另一個特殊功能是它們的區塊範圍。
在嚴格模式中,當函數宣告在程式碼區塊內時,它在該區塊內的任何地方都可見。但區塊外不可見。
例如,讓我們想像我們需要宣告一個函數 welcome()
,它取決於我們在執行期間取得的 age
變數。然後我們計畫稍後使用它。
如果我們使用函數宣告,它將無法按預期執行
let age = prompt("What is your age?", 18);
// conditionally declare a function
if (age < 18) {
function welcome() {
alert("Hello!");
}
} else {
function welcome() {
alert("Greetings!");
}
}
// ...use it later
welcome(); // Error: welcome is not defined
這是因為函數宣告僅在其所在的程式碼區塊內可見。
以下是另一個範例
let age = 16; // take 16 as an example
if (age < 18) {
welcome(); // \ (runs)
// |
function welcome() { // |
alert("Hello!"); // | Function Declaration is available
} // | everywhere in the block where it's declared
// |
welcome(); // / (runs)
} else {
function welcome() {
alert("Greetings!");
}
}
// Here we're out of curly braces,
// so we can not see Function Declarations made inside of them.
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(); // ok now
或者我們可以使用問號運算子 ?
進一步簡化它
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
function() { alert("Hello!"); } :
function() { alert("Greetings!"); };
welcome(); // ok now
原則上,當我們需要宣告函式時,第一個要考慮的是函式宣告語法。它在如何組織我們的程式碼方面提供了更大的自由度,因為我們可以在宣告函式之前呼叫它們。
這對於可讀性也更好,因為在程式碼中查詢 function f(…) {…}
比 let f = function(…) {…};
容易。函式宣告更「引人注目」。
…但是,如果函式宣告由於某種原因不適合我們,或者我們需要條件宣告(我們剛剛看過一個範例),則應該使用函式表達式。
摘要
- 函式是值。它們可以在程式碼的任何地方指派、複製或宣告。
- 如果函式在主程式碼流程中宣告為單獨的陳述式,則稱為「函式宣告」。
- 如果函式建立為表達式的一部分,則稱為「函式表達式」。
- 函式宣告在執行程式碼區塊之前處理。它們在區塊中的任何地方都可見。
- 函式表達式在執行流程到達它們時建立。
在我們需要宣告函式的大多數情況下,函式宣告是較佳的選擇,因為它在宣告本身之前可見。這讓我們在程式碼組織方面有更大的彈性,而且通常更具可讀性。
因此,我們應該僅在函式宣告不適合任務時才使用函式表達式。我們在本章中已經看過幾個範例,未來還會看到更多。
留言
<code>
標籤,若要插入多行程式碼,請將它們包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)