注:此文只在理解當即執行函數,不在所謂原創,文中大量引用阮一峯的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只存在於閉包)