在 JavaScript 中,函數不是「神奇的語言結構」,而是一種特殊的值。javascript
咱們在前面章節使用的語法稱爲 函數聲明:html
function sayHi() {
alert( "Hello" );
}
複製代碼
另外一種建立函數的語法稱爲 函數表達式。java
一般會寫成這樣:react
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
兩個 arguments 對象能夠被稱爲 回調函數 或簡稱 回調。
主要思想是咱們傳遞一個函數,並指望在稍後必要時將其「回調」。在咱們的例子中,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 語言的思想。
字符串或數字等常規值表明 數據。
函數能夠被視爲一個 動做。
咱們能夠在變量之間傳遞它們,並在須要時運行。
讓咱們來總結一下函數聲明和函數表達式之間的主要區別。
首先是語法:如何經過代碼對它們進行區分。
函數聲明:在主代碼流中聲明爲單獨的語句的函數。
// 函數聲明
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(…) {…}
更容易。函數聲明更「醒目」。
……可是,若是因爲某種緣由而致使函數聲明不適合咱們(咱們剛剛看過上面的例子),那麼應該使用函數表達式。
在大多數狀況下,當咱們須要聲明一個函數時,最好使用函數聲明,由於函數在被聲明以前也是可見的。這使咱們在代碼組織方面更具靈活性,一般也會使得代碼可讀性更高。
因此,僅當函數聲明不適合對應的任務時,才應使用函數表達式。在本章中,咱們已經看到了幾個例子,之後還會看到更多的例子。
本文首發於在微信公衆號「技術漫談」,歡迎關注。
現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 並列的 JavaScript 學習教程。
在線免費閱讀:zh.javascript.info
掃描下方二維碼,關注微信公衆號「技術漫談」,訂閱更多精彩內容。