前言
本篇要介紹的是在 JavaScript 常常會看到的問題,什麼是 IIFE ?
立即呼叫的函式表示式 ( Immediately Invoked Function Expression 簡稱 IIFE )
在前面的篇章,我們已經了解函式陳述句與函式表示式的差異。
標準的函式陳述句
1 | function greet(name){ |
這是標準函式陳述句,當 JavaScript 看到這個會將它放入記憶體中,等待被呼叫才開始執行內容。
標準的函式表示式
1 | var greetFunc = function(name) { |
這是標準函式表示式,一開始 JavaScript 並不會將函式的部分放入記憶體,而是在執行該行程式碼時,立即地創造這個函式物件,然後可以使用指向該函式位址的變數呼叫它。
同上,如果想立刻創造、同時也呼叫呢?
想想我們是如何呼叫一個函式的 ?
是使用「()」,我們已經達成立即創造函式物件了,如果在同一行補上()的話會怎麼樣?
於是可以這麼做:
1 | var greetFunc = function(name) { |
的確被執行了,可以讓執行結果更好一些,仿照先前函式帶入參數的方式。
1 | var greetFunc = function(name) { |
這就是立即呼叫的函式表示式 ( IIFE ),透過推導可以得知原理並不困難,就是函式表示式在創造後立刻呼叫它。
現在把程式碼修改並觀察其他部分
1 | var greetFunc = function(name) { |
如果改成這樣子寫,我們會得到 greetFunc
內函式的程式碼內容,加上 () 則呼叫該函式,一切都如我們預期。
但如果加入了立即呼叫呢?
1 | var greetFunc = function(name) { |
函式物件被函式表示式創造,然後被立即呼叫,接著值被回傳給 greetFunc
,所以輸出是 Hello John 。
但要注意的是,此時 greetFunc
是個字串,不是函式了,因為函式物件創造後又立刻被執行回傳字串給 greetFunc
。
1 | var greetFunc = function(name) { |
為什麼剛剛那個範例可以多次被呼叫,而不會變成字串呢?
1 | var greetFunc = function(name) { |
在這個例子因為,先前提到,一開始 JavaScript 並不會將函式的部分放入記憶體,而是在執行到該行程式碼時,立即地創造這個函式物件。
所以當執行到這一行時,
var greetFunc = function(name) {
因為只是創造匿名函式物件並沒有執行,所以此時的 greetFunc 變數的值指向匿名函式的記憶體位址。
JavaScript 中的表示式
在 JavaScript 中,表示式可以這麼寫,雖然沒作用,不過是正確的表示式:
1 | 3; |
可以觀察到像是數字、字串、物件都能像這樣直接使用表示式,那函式呢?
1 | function(name) { |
問題在於語法解析器先看到 function 這個字,語法解析器認為我們應該是要使用陳述句,然而這個陳述句卻缺少了名稱。陳述句不可以是匿名的,所以語法解析器認為這有問題。
但實際上,我們想做的是不藉由其他變數,單獨的讓這個函式表示式在這。
那是不是只要讓語法解析器不要第一個看到 function 就可以了?
於是我們這麼做,最簡單的就是把這些都包進 () 裡:
1 | (function(name) { |
現在就不會報錯了,現在語法解析器知道這個包在括號內的函式不是陳述句了,而是表示式。
調整程式碼並觀察
1 | (function(name) { |
順帶一提,結尾的()也可以寫在這邊,這兩者都是對的,只要保持一致就可以了。
IIFE 可以做什麼
我們知道 JavaScript 有全域執行環境、函式執行環境,直到 ES6 才出現塊級作用域(例如 let ),在 ES6 出來前,為了避免設定太多的全域變數,開發者往往會將變數設定在函式中,使其成為區域變數,尤其是設定在 IIFE 中,確保不會汙染到全域環境的變數。
1 | var firstName = 'Emma'; |
即使使用同樣的變數 firstName
,但 Doe
只存在於 IIFE 內,不會影響到外部環境的變數值 Emma
。
那如果反過來呢? IIFE 內想取用同樣名稱的變數值
1 | var firstName = 'Emma'; |
也只需要把全域物件 window
傳入即可。
經典例子
這是一個蠻常看到的經典例子,主要是一些觀念的綜合題。
1 | for(var i = 0; i < 10 ; i++){ |
情況是這樣的,該如何修改才能正確地使執行第 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 | for(let i = 0; i < 10 ; i++){ |
解法二:使用 IIFE
1 | for(var i = 0; i < 10 ; i++){ |
透過 IIFE 建立個別的執行環境,讓傳入的 i
值每個都可以被保存,讓 setTimeout
內的匿名函式向外尋找變數 i
時會先找到 IIFE 內的,因此就不會被外部環境的 i
影響了。