Immediately Invoked Function Expressions (IIFE)s
首先我們先來看看 :
- function statement
- function expression
1 | // function statement |
這兩個部分上面都有介紹過,主要區別在於是否有產生值
(IIFE)S
接下來看看這個範例:
在創造這個函式的當下,就呼叫它,並且括號內可以放入參數使用,就同英文名字一樣,立即呼叫函式表達式 (IIFE)
1 | var greeting = function (name){ |
印出結果會是
1 | //hello john |
範例
這邊我使用兩個簡單的範例:
1 | 3; |
這樣印出的結果並不會報錯
然而,當我們想要印出 function 時
1 | function(name){ |
卻會得到錯誤並且說明必須給 function statement 名字
所以要讓 function 像是其他純值以及物件一樣可以放置在程式碼中不報錯可以使用括號
這樣一來 syntax parser 就不會判定 function 必須入名字了
1 | var firstname = 'john' |
可以正常印出 indside IIFE: hello john
最後面 invoke 的括號可以寫在大括號內或是外都可以,記得一個方式持續使用即可
Framework Aside: IIFEs and Safe Code
IIFEs 內部的 EC 不會被外部環境影響,所以說它是安全的
這邊的 Hola 並不會影響到 IIFEs 內部的程式碼,因此這邊印出來的內容會是
Hello John
,而不是 Hola
從 IIFEs 內部影響 global EC
可以從下方範例看到,這邊的全域變數 greeting 被 IIFEs 內部變操作影響到了其結果,透過傳入 window 為參數對其操作的結果
1 |
|
印出結果
Understanding Closures
外部函式儘管已經被跳出執行堆,其變數還是可以被內部函式環境保留(當作 outer reference 使用),也就是確保範圍鍊可以正常使用
首先我來們觀察這段程式碼可以發現他 invoke 兩次,第一次傳入參數 Hi ,第二次因為 return 了下一個函式表達式所以可以再次傳入參數 Tony ,最後也沒有報錯正常印出
1 | function greet(whattosay){ |
印出結果:
奇怪的事情發生了:
這邊我把 greet()
指派給 sayHi ,並且在使用 sayHi('Tony')
,照理來說因為 sayHi 的 EC 執行完畢之後,就會跳出執行堆,因此這邊的 sayHi 執行結果應該是 undefined Tony ,奇怪的事情發生了,它卻正常執行印出了 Hi Tony
1 | function greet(whattosay){ |
印出結果:
這樣明明已經 EC 都已經彈出的狀態卻又把其變數保留的狀態就是 closures ,也就是JS引擎原生的特性
背後的原理
下圖說明了,當 var sayHi = greet('Hi');
執行完時,其實已經把當下的 EC 彈出了,只剩下 global EC
接下來執行下一行, sayHi('Tony')
,然而其創建的 EC 會保留他的 outer reference 也就是 whattosay 變數的內容,即使那個變數身處的 EC 已經消失,這就是 closure
Understanding Closures Part2
這邊用一個經典的例子解釋 Closures
從這邊的程式碼會得出甚麼結果呢?
- 建立一個 buildFunc 函式
- 創立空陣列 arr
- 使用 for loop 把匿名函式內容推進 arr 總共推了三次
- 函式會返回 arr 並且內部有三個匿名函式並且都會執行
console.log(i)
- 把函式 buildFunc 指派給 fn
- 使用
fn[i]()
的方式呼叫被推進 arr 內的函式
1 |
|
解釋背後發生什麼
首先 JS 引擎讀取程式碼並且把它們放入全域環境中
全域環境中目前包含了
buildFunctions()
var fs = buildFunctions()
- 三個
fs[]()
下一步因為 buildFunctions () 被執行了因此產生其執行背景 EC
在這段 EC 之中 for loop 跑完了並且要 return arr 時:
- i 的值跑到 3 之後跳出迴圈
- arr 的值被推進去了三個匿名函式(這邊注意函式並沒有被執行)
- 從這邊可以理解 i 其實就是跑完 for loop 的結果也就是 3
- arr 則是三個被推進去的匿名函式
1 | function buildFunctions() { |
會得到
重點
這個 EC 情況下記憶體中存在的 i 的值 以及 arr 的內容如下:
- i = 3
- arr 內容長這樣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17arr = [
//this is the first function pushed.
function(){
console.log(i);
},
//this is the second function pushed.
function(){
console.log(i);
},
//this is the third function pushed.
function(){
console.log(i);
}
];
buildFunctions 函式執行結束
當 buildFunctions 函式執行結束,它的 EC 就會跳出,但是因為 JS Closures 的特性,它們的變數值會被當作 outer reference 被留下也就是3
輪到執行 fs[i]()
照順序先從 fs[0]()
開始
- 一樣一開始建立 EC
- 但是在這個 EC 中是沒有 變數 i 因此 scope chain 就會觸發
- 也就會去尋找它的 outer reference 也就是存在 buildFunctions 函式內部的 i = 3
- 因此印出結果是 3
- 執行結束後 EC 跳出
- 執行
fs[1]()
- 以下以此類推……
- 由於他們的 outer reference 都是相同的因此印出的結果都是 3
如果不想要 Closures 發生呢
ES6 解法
1 | function buildFunctions2() { |
使用 ES6 的語法 let 來操作:
因為 let 的作用域在大括號內,所以每次 for loop 執行時,都會有一個新的變數 j 存在記憶體中,也就會保留不同的 i 的值
ES5解法
1 | function buildFunctions2() { |
這時候的 arr
變成三個立即函式
1 | arr = [ |
因為 IIFEs 的關係(立即執行函式)一樣會產生三個不同的 EC 並且保留不同的 j 當作 fs2[i]()
的 outer reference 就可以達成印出結果不為 3
結論
這邊就是 Closures 確保我們內部的函式可以取用到值當我們在最底部(fs[i]()
)執行它時,再好好的觀看上面的程式範例確保有看懂,那就表示你能理解 JS 進階的程式觀念
Framework Aside: Function Factories
使用 closures 來為函式製造一些預設的變數
使用 closures 來製造 pattern 讓函式更方便
1 | function makeGreeting(language){ |
從 greetEnglish, greetSpanih 的指派中可以理解,因為各呼叫了一次因此產生了兩個 EC 並且留住了兩個變數 en, es 給下面的 invoke 使用,雖然它們兩個在執行的時候都是同一個函式,但是因為個別的 EC 以及個別的記憶體空間內容,讓 closures 可以抓到不一樣的變數內容
於是我們執行得出的結果是
1 | Hello John Doe |
這邊的 makeGreetin 函式,就是所謂的 factory function,我們利用 closures 的特性,讓內部的匿名函式可以取得到外部函式的 language 變數
接下來我創造出了 en, es 讓 makeGreeting 函式可以在指派給不同變數的情況下利用這些不同的參數,所以也可以創造比方說 CH, JP 等等的參數就可以重複利用這段程式碼達到 factory 的效果
Closures and Callbacks
setTimeout
setTimeout 本身就是使用函式表達式把函式當作參數使用(first-class function 的一種特性)
因為 event loop 的操作會使得 web api 的內容在執行堆操作結束之後才會從 event queue 內部接下一個工作,因此 sayHiLater 會從執行堆彈出,但是因為 closures 的緣故,保留了變數 greeting 給 setTimeout 內的 console 使用
1 |
|
jQuery
在 click 部份一樣使用函式表達式把函式當作參數使用(first-class function 的一種特性)
1 |
|
callback functions
將函式作為參數傳遞到另一個函式時,被當作參數傳遞的那個函式我們稱之為回調函式 Callback function
1 |
|
call(), apply() and bind()
functions 是一種特殊的物件,包含
- name(可以匿名)
- code 程式碼內容(可調用的 invocable)
- call()
- apply()
- bind()
bind() 範例
從下方程式碼中 logName()
是會報錯的,因為裡面的 this 指向全域但是全域中卻沒有getFullname 這個 method ,會出現 this.getfullname is not a function at logName
1 |
|
這時就可以使用 bind()
它會製造出一個複製的 function 並且讓括號內的參數變為 this 的指向
方法一
把 logName 綁訂到 person 物件上面改變 this 的指向到 person 身上,就能正確印出 John Doe 搂 !
注意這邊 logName 沒有調用的緣故是把 function 當作物件使用 bind 這個方法,如果調用了則變成 logName 裡面的值則無法使用此方法
1 | var logNameRights = logName.bind(person) |
方法二
也可以直接使用在 logName 後面直接使用 bind(person)
也能正確印出 John Doe
1 | var logName = function (lang1, lang2) { |
放入參數
即使是操作 bind()
後,也還是可以帶入參數並且正確印出內容
1 | var logName = function (lang1, lang2) { |
call()
範例
- 相較於
bind()
的複製一個 function 操作 call()
則是直接執行函式
1 | var logName = function (lang1, lang2) { |
apply()
範例
基本上跟 call()
是一樣的操作方式只是,參數的部分必須以陣列的方式放入
1 | var logName = function (lang1, lang2) { |
也可以使用 IIFEs 操作
一樣可以印出結果
1 | (function (lang1, lang2) { |
實際應用
function borrowing
借用 function getFullName 從 person ,用來印出 person2 的全名 Jane Doe
1 | var person2 = { |
function curring (只能操作在 bind)
創造一個 copy 的函式並且有著固定的預設參數
已知 bind 內的第一個參數會是 this 的指向
之後的參數則為使用函式的固定參數
以下面程式碼為例:
1 |
|
bind(this,2)
1 | function multiply(a, b) { |
則 a 為 2
bind(this,3)
1 | function multiply(a, b) { |
則 a 為 3
藉由這樣的方式固定操作函式的參數讓其固定就被稱作 function curring