JavaScript Weird Part (33)- 立即呼叫的函數表示式 (IIFEs)

立即呼叫的函數表示式(immediately invoked function expression, IIFEs) 是一個常用的簡潔觀念。

先前我們已經知道函數陳述句和函數表示式的差異

函數陳述句

JavaScript 即便把函數放到記憶體中還是不會執行任何東西,呼叫它才會執行。

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

greet();

函數表示式

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

JavaScript 一開始不會把函數表示式部分被放進記憶體裡面,而是在執行該行程式碼的時候,立即地創造這個函數物件,然後可以使用指向該函式記憶體位置的變數呼叫它。

如何觸發函式表示式?我們需要指向那個物件,告訴它要執行程式,像這樣

1
greetFunc("Jacob");

因為變數已經知道函數記憶體的位址,所以只要加上 (),就可以觸發函式。

立即呼叫的函數表示式 IIFE

既然我們是使用 () 呼叫函數,現在我們已經創造函數了,如果我們在
函數物件的尾巴加上一個括號 (),結果會如何呢?像這樣:

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

仿照之前帶入參數的方式

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

立即呼叫函數表示式(IIFE)的原理並不複雜,就是在創造函數的時候,立刻呼叫它。

修改程式碼的其他觀察

1
2
3
4
5
var greeting = function(name) {
return "Hello" + name;
};

console.log(greeting); //函數的程式碼內容

會回傳 greeting 函數的內容

然後呼叫它

1
console.log(greeting("John"));

得出預期的值

可是如果加入立即呼叫呢?會怎樣?

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

console.log(greeting);

函數表達式創造函數物件,接著立即呼叫,值會回傳給 greeting, 所以輸出 Hello John。

要注意的是 greeting 現在是一個字串不是函數了,所以如果試著呼叫它會報錯。這是因為函數物件創造後立刻執行回傳字串給greeting

那為什麼剛剛的例子可以多次呼叫,可是不會變成字串呢?

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
3;
("I am a string");
{
name: "John";
}

數字、字串、物件都能像這樣直接使用表達式,那函式呢?

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

這是因為語法解析器看到 function 這個字,它預期應該是要使用函數陳述句,但卻缺少它所需要的名稱,函數陳述句不可以是匿名的,所以這個語法有問題。

但事實上我們所要的就只是不用其他變數,單獨讓函數表達式在這裡,那就確保語法解析器不要在程式碼的第一個字就碰到 function ? 像這樣做

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

現在就不會報錯了,現在語法解析器知道這個包在括號內的函式不是陳述句了,而是表示式。因為括號在 JavaScript 是一個運算子,括號通常都是用在一些表示式上,像是 (3+4)*2 或是把一些東西群組起來。像是 if 這樣的陳述句就不會用到括號。JavaScript 引擎知道在括號裡的東西是表示式,他假設你寫的函數是函數表示式,你正在創造函數物件。

進一步觀察

1
2
3
4
5
6
7
8
9
10
11
(function(name) {
var greeting = "Hello";
console.log('Hello ' + name);
})('Tony'); //Hello Tony

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

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

IFFE 的用處

我們知道 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 是屬於全域執行環境下的全域變數。

然而寫在 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 影響了。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :