立即執(zhí)行函數(shù)(Immediately Invoked Function Expression,IIFE)
在JS中,我們經(jīng)常會聽到一個名詞“立即執(zhí)行函數(shù)”,它們通常會以如下的語法形式出現(xiàn):
//IIFE普通函數(shù)
(function add(x,y){
console.log(x+y)
})(1,2);
(function add(x,y){
console.log(x+y)
}(1,2));
//IIFE匿名函數(shù)
(function(x,y){
console.log(x+y)
}(1,2));
(function(x,y){
console.log(x+y)
})(1,2);
IIFE函數(shù)的定義方式,可以讓函數(shù)在定義后就直接執(zhí)行,我們可以看到上面的代碼復(fù)制到console中后,不需要另外調(diào)用函數(shù),就會直接打印3。
對比下普通的函數(shù)聲明和調(diào)用方法
function add(x,y){
console.log(x+y)
}
add();
//如果函數(shù)聲明之后直接調(diào)用,調(diào)用語句會被無視
function add(x,y){
console.log(x+y)
}(1,2);
//匿名函數(shù)賦值給變量,可以正常調(diào)用
var addFunc = function(x,y){ console.log(x+y)};
addFunc(1,2);
//如果直接聲明匿名函數(shù)會直接報錯
function(){ console.log('hahaha')};
function(){ console.log('hahaha')}();
//Uncaught SyntaxError: Function statements require a function name
function test(){ console.log('hahaha')}();
//Uncaught SyntaxError: Unexpected token ')'
我們可以看出,如果沒有外面的()把函數(shù)體包裹起來,函數(shù)是沒有辦法立刻執(zhí)行的,匿名函數(shù)定義都會直接報錯,函數(shù)聲明后面跟著調(diào)用操作也會直接報錯。
其中的原因是來自于:JS對于函數(shù)聲明和函數(shù)表達式語句的解析處理的方式是不同的。
函數(shù)聲明(FunctionDeclaration )與函數(shù)表達式(Function Expression)
在JS中要定義一個函數(shù),有這么幾種方法:
- 函數(shù)構(gòu)造函數(shù)
使用構(gòu)造函數(shù)的過程,我們可以理解為是調(diào)用表達式,創(chuàng)建了新的對象實例,獲得的sum對象剛好是個Function的實例。
var sum = new Function('a','b', 'return a + b;');
alert(sum(10, 20)); //alerts 30
- 函數(shù)聲明
function sum(a, b){
return a + b;
}
alert(sum(10, 10)); //20
JS中如果形如行首以function關(guān)鍵字開頭,且?guī)в泻瘮?shù)名(Identifier),則解析為是函數(shù)聲明。如以下形式:
function Identifier ( FormalParameterListopt ){ FunctionBody }
所以,當我們直接定義一個匿名函數(shù),因為也是function開發(fā),JS會按照函數(shù)聲明的標準去解析它,解析后發(fā)現(xiàn)它沒有設(shè)置函數(shù)名,就會直接報錯。
JS在解析函數(shù)聲明的時候,存在變量提升的情況。
sum(1,2);//sum還未定義,也可以輸出3
console.log(sum);
function sum(x,y){
console.log(x+y);
}
- 函數(shù)表達式
使用函數(shù)表達式來獲得一個函數(shù)。
var sum = function(a, b) { return a + b; }
alert(sum(5, 5)); // alerts 10
var sum = function sumCopy(a, b) { return a + b; }
alert(sum(5, 5)); // alerts 10
JS中=是賦值操作符,說明該語句是一句表達式而不是聲明。表達式后面跟著的函數(shù),可以帶函數(shù)名也可以不帶。
函數(shù)表達式和函數(shù)聲明有什么區(qū)別呢?
我們看下兩段代碼的對比:
- 函數(shù)聲明
function sumDeclaration(a, b) {
console.log(typeof sumDeclaration);
return a + b;
}
var sum = sumDeclaration;
sum(1,2);//輸出function
console.log(typeof sumDeclaration);//輸出function
sumDeclaration函數(shù)聲明之后,它是被掛在外層的全局變量當中的,它是屬于它所在的整個上下文的,所以我們不僅可以在它的函數(shù)內(nèi)部去訪問到它,也可以在外層上下文訪問到它。
- 普通表達式
(1+3);
var a = 1+5;
console.log(a);
(var a = 1+5);//Uncaught SyntaxError: Unexpected token 'var'
當我們在console里面直接執(zhí)行表達式代碼的時候,我們會發(fā)現(xiàn)操作符會自動執(zhí)行,所以a會直接輸出5。1+3輸入也會去計算。
所以如果JS解析到是個表達式語句,它會去把語句中可執(zhí)行的操作符執(zhí)行。
我們發(fā)現(xiàn),如果把帶有聲明的語句如var等,用()包裹起來,會直接報錯。
此時JS不會把這個語句當作聲明去解析,而是當作表達式來解析,表達式中又沒有var的執(zhí)行邏輯,就報錯了。
- 函數(shù)表達式
console.log(sum);//undefined
console.log(sumCopy);//Error:sumCopy is not defined
var sum = function sumCopy(a, b) {
console.log(typeof sumCopy);
return a + b;
}
sum(1,2);//輸出function
console.log(sumCopy);//Error:sumCopy is not defined
我們會發(fā)現(xiàn),在定義之前我們可以訪問到sum變量,因為它已經(jīng)被var聲明了,也存在變量提升,所以sum獲取到的是undefined,但是sumCopy在第三行代碼執(zhí)行之前引用就會直接報錯。
因為,JS解析函數(shù)表達式的時候是按照順序在執(zhí)行解析的,沒有存在變量提升。
而且sumCopy這個函數(shù)沒有被當成函數(shù)聲明被解析,對于它的外部環(huán)境來說,它不會作為一個函數(shù)變量掛在到外部環(huán)境的詞法環(huán)境里面。因為它認為你在執(zhí)行一個具體的操作語句而不是聲明操作。
但是function關(guān)鍵詞會創(chuàng)建新的作用域,所以在sumCopy內(nèi)部再訪問它,會輸出function,因為sumCopy在它自己新創(chuàng)建的作用域里面函數(shù)變量名掛在到它自己的詞法作用域了。
我的理解是,當使用函數(shù)表達式的時候,可以理解為給sum這個變量賦值一個function對象。除非你引用sum這個對象,否則這個function對象,不能夠被它表達式語句作用域以外的作用域去使用。
Why do you need to invoke an anonymous function on the same line?
深入理解javascript中的立即執(zhí)行函數(shù)(function(){…})()
所以如果我們希望,函數(shù)定義完就執(zhí)行掉,不要被外層的任何對象保存??梢栽趺磳崿F(xiàn)呢?
結(jié)合前面說的表達式會執(zhí)行操作符和函數(shù)表達式的特點,所以在JS中,我們只要讓JS在解析的時候,不要認為這是函數(shù)聲明,而認為是函數(shù)表達式,并且不要把函數(shù)表達式保存下來不就可以了?
IIFE實現(xiàn)
我們可以借助普通表達式,把函數(shù)聲明改造成函數(shù)表達式。
-function test(){console.log('test');}();
+function test(){console.log('test');}();
(function test(){console.log('test');}());
console.log(test);//Uncaught ReferenceError: test is not defined
我們發(fā)現(xiàn),這三個語句都可以成功執(zhí)行,而且test函數(shù)沒有被外部保存下來。
所以說,關(guān)鍵就在于讓JS認為這句代碼不是個函數(shù)聲明而是個表達式。
(function test(){console.log('test');})()
(functionbody)()這種形式,可以理解為()讓JS認為內(nèi)部是個函數(shù)表達式,所以返回了個函數(shù)對象,functionObj()就直接執(zhí)行了。
[譯] JavaScript:立即執(zhí)行函數(shù)表達式(IIFE)
IIFE作用域問題
在ECMA文檔中,當函數(shù)表達式執(zhí)行的時候,我們可以看到,會把當前詞法環(huán)境的變量,賦給執(zhí)行上下文的詞法環(huán)境。也可以解釋為,它會把當前外層上下文的變量保存起來。


所以在這段代碼當中:
for(var i = 0;i < 5; i++ ) {
(function(i){
setTimeout(function(){
console.log(i);
},1000)
})(i);
}
通過()把function(i)變成了函數(shù)表達式,調(diào)用時傳入了每輪循環(huán)中的i,每輪循環(huán)中i的數(shù)值會被函數(shù)表達式保存在執(zhí)行上下文的詞法環(huán)境當中,所以每次執(zhí)行的時候,在事件循環(huán)隊列當中的每個任務(wù),對應(yīng)的詞法環(huán)境都是對應(yīng)的循環(huán)時的變量。
ECMA-sec-function-definitions-runtime-semantics-evaluation
What is an IIFE in JavaScript?
JS中的IIFE
JavaScript系列之立即執(zhí)行函數(shù)IIFE