函數聲明和函數表達式有什麼區別與聯繫,應該怎樣選擇和使用?

函數表達式

在 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. (1) 行聲明建立了函數,並把它放入到變量 sayHi
  2. (2) 行將 sayHi 複製到了變量 func。請注意:sayHi 後面沒有括號。若是有括號,func = sayHi() 會把 sayHi() 的調用結果寫進func,而不是 sayHi 函數 自己。
  3. 如今函數能夠經過 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。在瀏覽器中,這樣的函數一般會繪製一個漂亮的提問窗口。但這是另一件事了。

askshowOkshowCancel 兩個 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 語言的思想。


一個函數是表示一個「動做(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
複製代碼

咱們怎麼才能讓 welcomeif 外可見呢?

正確的作法是使用函數表達式,並將 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


掃描下方二維碼,關注微信公衆號「技術漫談」,訂閱更多精彩內容。

相關文章
相關標籤/搜索