立即執行函數,就是在定義函數的時候直接執行,這里不是申明函數而是一個函數表達式
1.問題
在javascript中,每一個函數在被調用的時候都會創建一個執行上下文,在函數內部定義的變量和函數只能在該函數內部調用,正是因為這個上下文,使得在調用函數的時候可以創建一些私有變量。如下代碼
//makeCounter,返回一個新的函數(閉包),這個函數可以訪問makeCounter里的局部變量i function makeCounter() { var i = 0; return function () { document.write(++i); document.write('<br>'); } } //counter1和counter2是不同的實例,分別擁有自己范圍內的變量i var counter1 = makeCounter(); counter1(); counter1(); var counter2 = makeCounter(); counter2(); counter2();
這里i是函數makeCounter函數內的局部變量,所以定義的counter1和counter2都有自己的變量i,上面代碼輸出結果如下:
注意閉i始終保存在內存中,所以第二次調用的時候輸出的是2。
普通情況下我們定義一個函數,然后在語句中函數名字后面加上一對圓括號就可以直接調用它,能不能定義完之后直接在后面加上小括號調用呢?如下
function(){ counter1(); }(); // SyntaxError: Unexpected token (
答案是不行,這樣會報錯的。為什么呢?在javascript解釋代碼的時候,遇到function關鍵字的時候就認為這里是一個函數聲明,而不是函數表達式,如果沒有顯式地定義成函數表達式就會報錯,因為函數聲明需要一個函數名,上面的代碼沒有函數名。
既然是因為沒有函數名字報錯那好就加上一個函數名,如下:
function foo(){ counter1(); }(); // SyntaxError: Unexpected token )
依然會報錯,為什么呢?在一個函數聲明語句(這次是正確的)后面加上一對圓括號,這對圓括號和前面的聲明語句沒有任關系,而只是一個分組操作符,用來控制運算的優先級,這里的意思是小括號里面優先計算,所以上面代碼等同於:
function foo(){ counter1(); } (); // SyntaxError: Unexpected token )
2.概念
正確的寫法是怎樣的呢?簡單,如下:
(function () { counter1(); }());
這樣為什么就可以呢?在javascript里圓括號內不能包含語句,當解釋器對代碼進行解釋的時候遇到圓括號就認為這里面是表達式,然后遇到function關鍵字就認為這是一個函數表達式,而不是函數聲明。而更加奇妙的是只要是能將后面語句預先解釋為表達式都可以,不一定是分分組操作符,於是立即執行函數表達式有了五花八門的寫法,如下:
(function () { counter1(); }()); (function () { counter1(); })(); var i = function(){ counter1(); }(); true && function () { counter1(); }(); 0, function(){ counter1() }(); !function () { counter1(); }(); ~function () { counter1(); }(); -function () { counter1(); }(); +function () { counter1(); }();
輸出結果如下:
甚至可以這樣:
new function(){ counter1(); } new function(){ counter1(); }() // 帶參數
這樣:
var i = function(){ counter1(); }(); var j = (function(){ return 10; }());
這是為什么呢?因為new,=是運算符,和+,-,*,/一個樣,都會把后面的語句預先解釋為表達式。這里推薦上面一種寫法,因為function內部代碼如果太多,我們不得不滾到最后去看function(){}后是否帶有()。
3.立即執行函數和閉包有什么關系
和普通函數傳參一樣,立即執行函數也可以傳遞參數。如果在函數內部定一個函數,而里面的那個函數能引用外部的變量和參數(閉包),我們就能用立即執行函數鎖定變量保存狀態。
我們在hmtl頁面中方兩個超鏈接標簽,然后用下面的代碼來測試:
<div> <ul> <li><a>第一個超鏈接</a></li> <li><a>第二個超鏈接</a></li> </ul> </div> var elems = document.getElementsByTagName('a'); for(var i=0; i<elems.length; i++) { elems[i].addEventListener('click', function (e) { e.preventDefault(); alert('I am click Link #' + i); }, 'false') }
這段代碼意圖是點擊第一個超鏈接提示“I am click Link #0”,點擊第二個提示“I am click Link #1”。真的是這樣嗎? 不是,每一次都是“I am click Link #2”
因為i的值沒有被鎖住,當我們點擊鏈接的時候其實for循環早已經執行完了,於是在點擊的時候i的值已經是elems.length了。
修改代碼如下:
var elems = document.getElementsByTagName('a'); for(var i=0; i < elems.length; i++){ (function (LockedInIndex) { elems[i].addEventListener('click', function (e) { e.preventDefault(); alert('I am cliick Link #' + i); }, 'false') })(i) }
這次可以正確的輸出結果,i的值被傳給了LockedIndex,並且被鎖定在內存中,盡管for循環之后i的值已經改變,但是立即執行函數內部的LockedIndex的值並不會改變。
還可以這樣寫:
var elems = document.getElementsByTagName('a'); for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', (function( lockedInIndex ){ return function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }; })( i ), 'false' ); }
但是我覺得如果用let是不是就可以一下子解決了:
var elems = document.getElementsByTagName('a'); for(let i=0; i<elems.length; i++) { elems[i].addEventListener('click', function (e) { e.preventDefault(); alert('I am click Link #' + i); }, 'false') }
let是塊級作用域內的變量,是es6新定義的,這里不展開。
4.模塊模式
立即執行函數在模塊化的時候也有用,用立即執行函數處理模塊可以減少全局變量造成的空間污染,而是使用私有變量。
如下創建一個立即執行的匿名函數,該函數返回一個對象,包含要暴露給外部的屬性i,如果不實用立即執行函數就要多定義一個屬性i了,這個i就會顯示的暴露給外部,這樣:counter.i,這種方式明顯不太安全。
var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function( val ){ i = val; }, increment: function() { return ++i; } }; }()); document.write('<br>'); document.write(counter.get());document.write('<br>'); document.write(counter.set( 3 ));document.write('<br>'); document.write(counter.increment());document.write('<br>'); // 4 document.write(counter.increment());document.write('<br>'); // 5
注意,這里如果使用counter.i來訪問這個內部變量,會報錯undefined,因為i並不是counter的屬性。
好了,就這么多。