1. 程式人生 > 其它 >JavaScript 基礎知識6 函式表示式

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(…) {…} 更容易。函式宣告更“醒目”。

……但是,如果由於某種原因而導致函式宣告不適合我們(我們剛剛看過上面的例子),那麼應該使用函式表示式。

總結
函式是值。它們可以在程式碼的任何地方被分配,複製或宣告。
如果函式在主程式碼流中被宣告為單獨的語句,則稱為“函式宣告”。
如果該函式是作為表示式的一部分建立的,則稱其“函式表示式”。
在執行程式碼塊之前,內部演算法會先處理函式宣告。所以函式宣告在其被宣告的程式碼塊內的任何位置都是可見的。
函式表示式在執行流程到達時建立。
在大多數情況下,當我們需要宣告一個函式時,最好使用函式宣告,因為函式在被宣告之前也是可見的。這使我們在程式碼組織方面更具靈活性,通常也會使得程式碼可讀性更高。

所以,僅當函式宣告不適合對應的任務時,才應使用函式表示式。在本章中,我們已經看到了幾個例子,以後還會看到更多的例子。