這裡介紹各種 JavaScript 函數的定義方式,有些方式很常見,但是有一些你可能沒看過。

以下是在 JavaScript 中四種建立函數的方式:

// 四種建立函數的方法
function declaration () {};
var funcExpression = function () {};
var namedFuncExpression = function named() {};
var fnConstructor = new Function ();

這些都是可以用來建立函數(Function)物件的方法,但是其中有些差異,以下我們將討論這些作法之間有什麼差別。

函數宣告(Function Declaration)

函數宣告是最常見的用法:

function fn () {
  // 函數內容 ...
}

如果使用這樣的方式來定義函數,則在整個程式中同一個 scope 之內的任何地方都可以使用這個函數,就算在這個函數定義之前也沒問題,就像這樣:

// 呼叫 fn()
fn();

function fn () {
  // 函數內容 ...
}

函數運算式(Function Expressions)

函數在 JavaScript 是一個一級物件(first class object),所以你可以用 JavaScript 的運算式(expression)來建立函數,而這樣的運算式就稱為函數運算式(function expressions)。

用這種方式定義函數也很簡單,就把函數的宣告放在一般運算式的位置,這樣就可以建立一個函數了,例如:

// 定義 fnExpr() 函數
var fnExpr = function () {
  // 函數內容 ...
};

除此之外,你也可以將這種方式應用在其他各種地方,例如放在括號中或是一元運算子之後:

// 匿名函數運算式(anonymous function expressions)
(function fnExpr2() {} );

!function fnExpr3() {}

JavaScript 的函數運算式可分為具名與匿名兩種,具名函數運算式(named function expressions,簡稱 NFE)會在函數內部建立一個儲存自己名稱的變數,而這個變數在函數之外是看不到的:

var namedFuncExpression = function named() {
  return named.name;
};

named();
// ReferenceError: named is not defined

namedFuncExpression();
// 傳回 "named"

具名函數運算式對於在除錯時會非常有用,在 JavaScript 的除錯環境的 stack traces、call stacks 或 中斷點(breakpoints)列表中,如果碰到匿名函數大概只會顯示 anonymous 這樣沒有用處的名稱,如果是具名函數的話,就會清楚標示該函數的名稱,這對於除錯而言是很好用功能。

對於 JavaScript 的函數式程式設計(functional programming)而言(例如 currying 與 partials),函數運算式也是非常關鍵的功能之一。

函數關鍵字(Function Keyword)

最後一種方式就是直接使用 Function 這個關鍵字來建立函數物件,在使用時將參數與函數的內容依序傳入 Function,然後就可以建立一個函數物件了。

這裡要注意的是,如果使用這樣的方式建立函數,不會建立任何的 closure,而在這個函數中只能存取全域(global)的變數或是存在於該函數內部的變數。

var add = new Function('a','b', 'return a + b');

add(1,2);
// 輸出 3

var glbFoo = "global";
function scope() {
  var scpFoo = "scoped",
    scop=Function('console.log(typeof scpFoo)'),
    glob=Function('console.log(typeof glbFoo)');

  scop();
  glob();
}

scope();
// 輸出 undefined
// 輸出 string

另外使用這樣的方式所建立的函數,在每一次執行時都會進行 parse 的動作,所以效率會比較差。

函數運算式與函數宣告的差異

如果以函數宣告的方式來建立函數,則這個函數會被提升(hoisted)到該 scope 的最頂端,所以可以讓整個 scope 都直接呼叫它,但如果是函數運算式就必須在函數定義之後,才可以使用:

declared();
// 輸出 declared
function declared () {
  console.log('declared');
}

expressed();
// TypeError: undefined is not a function

var expressed = function () {
  console.log('expressed ');
}
expressed();
// 輸出 expressed

區分函數運算式與函數宣告

函數運算式與函數宣告在語法上很相似,很容易讓人混淆,在辨別它們時需要注意一下。根據 ECMAscript 得標準,函數宣告必須有一個識別名稱(identifier),所以說只要函數沒有識別名稱,那麼它就是一個函數運算式,這個很容易分辨,而接來的問題就是如何分辨具名的函數運算式與函數宣告。

由於函數宣告只容許作為指令稿或是函數的 sourceElements,它不能出現在巢狀的 blocks 中,因為 blocks 中只容許放置 statements,不能放置 sourceElements。

var a = 0;
function b () {};
if (false) {
  // 巢狀結構,不是 source element
  var c = 0;
  function d () {};
}

// 函數宣告
function foo() {
  // 另一個函數宣告
  function bar() {};

  if(true) {
    // 不是 source element,所以是函數運算式
    function baz() {};
  }

  // 因為放在一元運算子之後,會被視為運算式
  // 所以這個是函數運算式
  !function bng () {};
};

參考資料:CODEKRAFT