[JavaScriptWeird]No.33 立即呼叫的函式表示式 IIFE

前言

本篇要介紹的是在 JavaScript 常常會看到的問題,什麼是 IIFE ?

立即呼叫的函式表示式 ( Immediately Invoked Function Expression 簡稱 IIFE )

在前面的篇章,我們已經了解函式陳述句與函式表示式的差異。

標準的函式陳述句

1
2
3
4
5
function greet(name){  
console.log('Hello ' + name);
}

greet('John'); // Hello John

這是標準函式陳述句,當 JavaScript 看到這個會將它放入記憶體中,等待被呼叫才開始執行內容。

標準的函式表示式

1
2
3
4
var greetFunc = function(name) {  
console.log('Hello ' + name);
}
greetFunc('John'); // Hello John

這是標準函式表示式,一開始 JavaScript 並不會將函式的部分放入記憶體,而是在執行該行程式碼時,立即地創造這個函式物件,然後可以使用指向該函式位址的變數呼叫它。

同上,如果想立刻創造、同時也呼叫呢?

想想我們是如何呼叫一個函式的 ?

是使用「()」,我們已經達成立即創造函式物件了,如果在同一行補上()的話會怎麼樣?

於是可以這麼做:

1
2
3
var greetFunc = function(name) {  
console.log('Hello ' + name);
}(); // Hello undefined

的確被執行了,可以讓執行結果更好一些,仿照先前函式帶入參數的方式。

1
2
3
var greetFunc = function(name) {  
console.log('Hello ' + name);
}('John'); // Hello John

這就是立即呼叫的函式表示式 ( IIFE ),透過推導可以得知原理並不困難,就是函式表示式在創造後立刻呼叫它。

現在把程式碼修改並觀察其他部分

1
2
3
4
5
6
var greetFunc = function(name) {  
return 'Hello ' + name;
};

console.log(greetFunc); //函式的程式碼內容
console.log(greetFunc('John')); // Hello John

如果改成這樣子寫,我們會得到 greetFunc 內函式的程式碼內容,加上 () 則呼叫該函式,一切都如我們預期。

但如果加入了立即呼叫呢?

1
2
3
4
5
var greetFunc = function(name) {  
return 'Hello ' + name;
}('John');

console.log(greetFunc); // Hello John

函式物件被函式表示式創造,然後被立即呼叫,接著值被回傳給 greetFunc ,所以輸出是 Hello John 。

但要注意的是,此時 greetFunc 是個字串,不是函式了,因為函式物件創造後又立刻被執行回傳字串給 greetFunc

1
2
3
4
5
6
var greetFunc = function(name) {  
return 'Hello ' + name;
}('John');

console.log(greetFunc());
console.log(typeof(greetFunc)); //string

為什麼剛剛那個範例可以多次被呼叫,而不會變成字串呢?

1
2
3
4
5
6
var greetFunc = function(name) {  
return 'Hello ' + name;
};

console.log(greetFunc('John'));
console.log(greetFunc('John'));

在這個例子因為,先前提到,一開始 JavaScript 並不會將函式的部分放入記憶體,而是在執行到該行程式碼時,立即地創造這個函式物件。

所以當執行到這一行時,

var greetFunc = function(name) {

因為只是創造匿名函式物件並沒有執行,所以此時的 greetFunc 變數的值指向匿名函式的記憶體位址。

JavaScript 中的表示式

在 JavaScript 中,表示式可以這麼寫,雖然沒作用,不過是正確的表示式:

1
2
3
4
5
6
3;  
1 + 2 + 3;
'我是字串';
{
name: 'Tony';
}

可以觀察到像是數字、字串、物件都能像這樣直接使用表示式,那函式呢?

1
2
3
function(name) {  
return 'Hello ' + name;
};

問題在於語法解析器先看到 function 這個字,語法解析器認為我們應該是要使用陳述句,然而這個陳述句卻缺少了名稱。陳述句不可以是匿名的,所以語法解析器認為這有問題。

但實際上,我們想做的是不藉由其他變數,單獨的讓這個函式表示式在這

那是不是只要讓語法解析器不要第一個看到 function 就可以了?
於是我們這麼做,最簡單的就是把這些都包進 () 裡:

1
2
3
(function(name) {  
return 'Hello ' + name;
});

現在就不會報錯了,現在語法解析器知道這個包在括號內的函式不是陳述句了,而是表示式。

調整程式碼並觀察

1
2
3
4
5
6
7
8
9
10
(function(name) {  
console.log('Hello ' + name);
})('John'); //Hello John

現在我們加入一點前面提到的觀念,直接呼叫它,這也是一個 IIFE。
另外這也是 IIFE 最常見的一種樣子。

(function(name) {
console.log('Hello ' + name);
}('John')); //Hello John

順帶一提,結尾的()也可以寫在這邊,這兩者都是對的,只要保持一致就可以了。

IIFE 可以做什麼

我們知道 JavaScript 有全域執行環境、函式執行環境,直到 ES6 才出現塊級作用域(例如 let ),在 ES6 出來前,為了避免設定太多的全域變數,開發者往往會將變數設定在函式中,使其成為區域變數,尤其是設定在 IIFE 中,確保不會汙染到全域環境的變數。

1
2
3
4
5
6
7
var firstName = 'Emma';  
(function(name) {
var firstName = 'Doe';
console.log('Hello ' + name + ' ' + firstName);
})('John'); // Hello John Doe

console.log(firstName); // Emma

即使使用同樣的變數 firstName ,但 Doe 只存在於 IIFE 內,不會影響到外部環境的變數值 Emma

那如果反過來呢? IIFE 內想取用同樣名稱的變數值

1
2
3
4
5
6
var firstName = 'Emma';  
(function(global) {
var firstName = 'Doe';
console.log('Hello ' + firstName); // Hello Doe
console.log('Hello ' + global.firstName); // Hello Emma
})(window);

也只需要把全域物件 window 傳入即可。

經典例子

這是一個蠻常看到的經典例子,主要是一些觀念的綜合題。

1
2
3
4
5
6
for(var i = 0; i < 10 ; i++){  
console.log(i);
setTimeout(function () {
console.log('執行第' + i + '次');
},10);
}

情況是這樣的,該如何修改才能正確地使執行第 i 次正確的輸出所有的 i 呢?

觀念是這樣的,因為寫在 for 迴圈內的 i 變數是使用 var 宣告的,而又沒有使用函式包覆,因此這個 i 是屬於全域執行環境下的全域變數。

1
console.log(window.i); // 10

然而寫在 setTimeout 內的匿名函式,因為沒有 i 變數,所以會轉而向外部環境尋找。

setTimeout

  • setTimeout 的作用就是把函式設定執行時間後,丟到事件佇列擱著。
  • for 迴圈是這樣處理 setTimeout 的:按照設定的方式,一次跑完,至於 setTimeout 的內容是什麼不管,以本例來說就是幾乎同時設定了 10 次 setTimeout。

所以才會在輸出幾乎同時看到「執行第10次」

解法一:使用 let

可以使用 ES6 新增的 let 輕鬆處理掉這個問題。因為 let 屬於區塊範圍 (Block Scope) ,變數僅存活於 {} 中,所以每次執行迴圈時取得的 i 在記憶體位址上都不同的,因此在 setTimeout 內的匿名函式參考到的 i 也都是不同的記憶體位址。

1
2
3
4
5
for(let i = 0; i < 10 ; i++){  
setTimeout(function () {
console.log('執行第' + i + '次');
},10);
}

解法二:使用 IIFE

1
2
3
4
5
6
7
8
for(var i = 0; i < 10 ; i++){  
(function(i) {
setTimeout(function () {
console.log('執行第' + i + '次');
},10);
})(i)
}
console.log(window.i); // 10

透過 IIFE 建立個別的執行環境,讓傳入的 i 值每個都可以被保存,讓 setTimeout 內的匿名函式向外尋找變數 i 時會先找到 IIFE 內的,因此就不會被外部環境的 i 影響了。

0%