JavaScript中的當即執行函數

注:此文只在理解當即執行函數,不在所謂原創,文中大量引用阮一峯的JavaScript標準參考教程MDN的JavaScript 參考文檔深刻理解JavaScript系列(4):當即調用的函數表達式的內容。javascript

描述

當即執行函數一般有下面兩種寫法:html

(function(){ 
   ...
})();
(function(){ 
    ...
}());

在Javascript中,一對圓括號「()」是一種運算符,跟在函數名以後,表示調用該函數。好比,print()就表示調用print函數。java

這個寫法和咱們想象的寫法不同(知道的人固然已經習覺得常)
不少人剛開始理解當即執行函數的時候,以爲應該是這樣的:express

function (){ ... }();

//或者

function fName(){ ... }();

然而事實倒是這樣:SyntaxError: Unexpected token (。這是爲何呢?閉包

解釋

要理解當即執行函數,須要先理解一些函數的基本概念:函數聲明函數表達式,由於咱們定義一個函數一般都是經過這兩種方式函數

函數聲明 (function 語句)優化

function name([param[, param[, ... param]]]) {
   statements
}

name:函數名;
param:被傳入函數的參數的名稱,一個函數最多能夠有255個參數;
statements:這些語句組成了函數的函數體。code

函數表達式 (function expression)htm

函數表達式和函數聲明很是相似,它們甚至有相同的語法。對象

function [name]([param] [, param] [..., param]) {
   statements
}

name:函數名,能夠省略,省略函數名的話,該函數就成爲了匿名函數
param:被傳入函數的參數的名稱,一個函數最多能夠有255個參數;
statements:這些語句組成了函數的函數體。

下面咱們給出一些栗子說明:

// 聲明函數f1
function f1() {
    console.log("f1");
}
// 經過()來調用此函數
f1();


//一個匿名函數的函數表達式,被賦值給變量f2:
var f2 = function() {
    console.log("f2");
}
//經過()來調用此函數
f2();


//一個命名爲f3的函數的函數表達式(這裏的函數名能夠隨意命名,能夠沒必要和變量f3重名),被賦值給變量f3:
var f3 = function f3() {
    console.log("f2");
}
//經過()來調用此函數
f3();

上面所起的做用都差很少,但仍是有一些差異
一、函數名和函數的變量存在着差異。函數名不能被改變,但函數的變量卻可以被再分配。函數名只能在函數體內使用。假若在函數體外使用函數名將會致使錯誤:

var y = function x() {};
alert(x); // throws an erro

二、函數聲明定義的函數能夠在它被聲明以前使用

foo(); // alerts FOO!
function foo() {
   alert('FOO!');
}

但函數聲明很是容易(常常是意外地)轉換爲函數表達式。當它再也不是一個函數聲明:

  • 成爲表達式的一部分

  • 再也不是函數或者script自身的「源元素」 (source element)。在script或者函數體內「源元素」並不是是內嵌的語句(statement)

var x = 0;               // source element
if (x == 0) {            // source element
   x = 10;               // 非source element
   function boo() {}     // 非 source element
}
function foo() {         // source element
   var y = 20;           // source element
   function bar() {}     // source element
   while (y == 10) {     // source element
      function blah() {} // 非 source element
      y++;               //非source element
   }
}

Examples:

// 函數聲明
function foo() {}

// 函數表達式 
(function bar() {})

// 函數表達式
x = function hello() {}


if (x) {
   // 函數表達式
   function world() {}
}


// 函數聲明
function a() {
   // 函數聲明
   function b() {}
   if (0) {
      //函數表達式
      function c() {}
   }
}

如今咱們來解釋上面的SyntaxError: Unexpected token (
產生這個錯誤的緣由是,Javascript引擎看到function關鍵字以後,認爲後面跟的是函數定義語句,不該該以圓括號結尾。
解決方法就是讓引擎知道,圓括號前面的部分不是函數定義語句,而是一個表達式,能夠對此進行運算。因此應該這樣寫:

(function(){ /* code */ }()); 

// 或者

(function(){ /* code */ })();

這兩種寫法都是以圓括號開頭,引擎就會認爲後面跟的是一個表示式,而不是函數定義,因此就避免了錯誤。這就叫作「當即調用的函數表達式」(Immediately-Invoked Function Expression),簡稱IIFE。

注意,上面的兩種寫法的結尾,都必須加上分號。

推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生一樣的效果,好比下面三種寫法。

var i = function(){ return 10; }();

true && function(){ /* code */ }();

0, function(){ /* code */ }();

甚至像這樣寫:

!function(){ /* code */ }();

~function(){ /* code */ }();

-function(){ /* code */ }();

+function(){ /* code */ }();

new關鍵字也能達到這個效果:

new function(){ /* code */ }

new function(){ /* code */ }() // 只有傳遞參數時,才須要最後那個圓括號。

使用

那咱們一般爲何使用函數當即表達式呢,以及我如何使用呢?

一般狀況下,只對匿名函數使用這種「當即執行的函數表達式」。
它的目的有兩個:

  • 一是沒必要爲函數命名,避免了污染全局變量;

  • 二是IIFE內部造成了一個單獨的做用域,能夠封裝一些外部沒法讀取的私有變量。

// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 寫法二
(function (){
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代碼中,寫法二比寫法一更好,由於徹底避免了污染全局變量。

最後在舉一個真實的栗子:在JavaScript的OOP中,咱們能夠經過IIFE來實現一個單例(關於單例的優化再也不此處討論)

// 建立一個當即調用的匿名函數表達式
// return一個變量,其中這個變量裏包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的function自身

var counter = (function () {
    var i = 0;

    return {
        get: function () {
            return i;
        },
        set: function (val) {
            i = val;
        },
        increment: function () {
            return ++i;
        }
    };
} ());

// counter是一個帶有多個屬性的對象,上面的代碼對於屬性的體現實際上是方法

counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5

counter.i; // undefined 由於i不是返回對象的屬性
i; // 引用錯誤: i 沒有定義(由於i只存在於閉包)
相關文章
相關標籤/搜索