# The Weird Part Of Javascript - part 3 ###### tags: `Javascript` # Immediately Invoked Function Expressions (IIFE)s 首先我們先來看看 : * function statement * function expression ```javascript= // function statement function greet(name){ console.log('hello' + name); } greet(); // invoke it //using a function expression var greetFunc = function (){ console.log('hello' + name); } greetFunc();// invoke it ``` 這兩個部分上面都有介紹過,主要區別在於是否有產生值 ## (IIFE)S 接下來看看這個範例: 在創造這個函式的當下,就呼叫它,並且括號內可以放入參數使用,就同英文名字一樣,立即呼叫函式表達式 (IIFE) ```javascript= var greeting = function (name){ return 'hello' + name; }('john'); console.log(greeting) ``` 印出結果會是 ```javascript= //hello john ``` ### 範例 這邊我使用兩個簡單的範例: ```javascript= 3; 'I am a string'; { name:'john' }; ``` 這樣印出的結果並不會報錯 然而,當我們想要印出 function 時 ```javascript= function(name){ var greeting='inside IIFE: hello'; console.log(greeting +' '+ name); } ``` 卻會得到錯誤並且說明必須給 function statement 名字 ![](https://i.imgur.com/p4urka8.png) 所以要讓 function 像是其他純值以及物件一樣可以放置在程式碼中不報錯可以使用括號 這樣一來 syntax parser 就不會判定 function 必須入名字了 ```javascript= var firstname = 'john' (function(name){ var greeting='inside IIFE: hello'; console.log(greeting +' '+ name); })('john'); ``` 可以正常印出 `indside IIFE: hello john` 最後面 invoke 的括號可以寫在大括號內或是外都可以,記得一個方式持續使用即可 # Framework Aside: IIFEs and Safe Code > IIFEs 內部的 EC 不會被外部環境影響,所以說它是安全的 ![](https://i.imgur.com/JyJn5qb.png) 這邊的 Hola 並不會影響到 IIFEs 內部的程式碼,因此這邊印出來的內容會是 `Hello John`,而不是 Hola ## 從 IIFEs 內部影響 global EC 可以從下方範例看到,這邊的全域變數 greeting 被 IIFEs 內部變操作影響到了其結果,透過傳入 window 為參數對其操作的結果 ```javascript= var greeting = 'hola'; var firstname = 'john'; (function(global,name){ global.greeting = 'hello'; console.log(greeting +' '+ name); }(window,firstname)); ``` 印出結果 ![](https://i.imgur.com/qjDmgWn.png) # Understanding Closures > 外部函式儘管已經被跳出執行堆,其變數還是可以被內部函式環境保留(當作 outer reference 使用),也就是確保範圍鍊可以正常使用 首先我來們觀察這段程式碼可以發現他 invoke 兩次,第一次傳入參數 Hi ,第二次因為 return 了下一個函式表達式所以可以再次傳入參數 Tony ,最後也沒有報錯正常印出 ```javascript= function greet(whattosay){ return function (name){ console.log(whattosay + ' ' + name); } }; greet('Hi')('Tony'); ``` 印出結果: ![](https://i.imgur.com/edkng8Z.png) 奇怪的事情發生了: 這邊我把 `greet()` 指派給 sayHi ,並且在使用 `sayHi('Tony')`,照理來說因為 sayHi 的 EC 執行完畢之後,就會跳出執行堆,因此這邊的 sayHi 執行結果應該是 undefined Tony ,奇怪的事情發生了,它卻正常執行印出了 Hi Tony ```javascript= function greet(whattosay){ return function (name){ console.log(whattosay + ' ' + name); } }; var sayHi = greet('Hi'); sayHi('Tony') ``` 印出結果: ![](https://i.imgur.com/edkng8Z.png) > 這樣明明已經 EC 都已經彈出的狀態卻又把其變數保留的狀態就是 closures ,也就是JS引擎原生的特性 ## 背後的原理 下圖說明了,當 `var sayHi = greet('Hi');` 執行完時,其實已經把當下的 EC 彈出了,只剩下 global EC ![](https://i.imgur.com/e2omTTN.png) 接下來執行下一行, `sayHi('Tony')` ,然而其創建的 EC 會保留他的 outer reference 也就是 whattosay 變數的內容,即使那個變數身處的 EC 已經消失,這就是 closure ![](https://i.imgur.com/PclVUlw.png) # Understanding Closures Part2 這邊用一個經典的例子解釋 Closures 從這邊的程式碼會得出甚麼結果呢? 1. 建立一個 buildFunc 函式 1. 創立空陣列 arr 2. 使用 for loop 把匿名函式內容推進 arr 總共推了三次 3. 函式會返回 arr 並且內部有三個匿名函式並且都會執行 `console.log(i)` 4. 把函式 buildFunc 指派給 fn 5. 使用 `fn[i]()` 的方式呼叫被推進 arr 內的函式 ```javascript= function buildFunctions(){ arr = []; for(i=0;i<3;i++){ arr.push(function(){ console.log(i); }); } return arr; } var fs = buildFunctions(); fs[0](); fs[1](); fs[2](); ``` ## 解釋背後發生什麼 首先 JS 引擎讀取程式碼並且把它們放入全域環境中 全域環境中目前包含了 * `buildFunctions()` * `var fs = buildFunctions()` * 三個`fs[]()` ![](https://i.imgur.com/WEoq5Kk.png) ### 下一步因為 buildFunctions () 被執行了因此產生其執行背景 EC 在這段 EC 之中 for loop 跑完了並且要 return arr 時: * i 的值跑到 3 之後跳出迴圈 * arr 的值被推進去了三個匿名函式(這邊注意函式並沒有被執行) * 從這邊可以理解 i 其實就是跑完 for loop 的結果也就是 3 * arr 則是三個被推進去的匿名函式 ```javascript= function buildFunctions() { arr = []; for (i = 0; i < 3; i++) { arr.push(function () { console.log(i); }); } return console.log(i), arr; } buildFunctions(); ``` 會得到 ![](https://i.imgur.com/F6Xk1wq.png) ### 重點 這個 EC 情況下記憶體中存在的 i 的值 以及 arr 的內容如下: * i = 3 * arr 內容長這樣 ```javascript= arr = [ //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); } ]; ``` ![](https://i.imgur.com/G4BrWvw.png) ### buildFunctions 函式執行結束 當 buildFunctions 函式執行結束,它的 EC 就會跳出,但是因為 JS Closures 的特性,它們的變數值會被當作 outer reference 被留下也就是3 ![](https://i.imgur.com/TuQvwYr.png) ### 輪到執行 `fs[i]()` 照順序先從 `fs[0]()` 開始 1. 一樣一開始建立 EC 1. 但是在這個 EC 中是沒有 變數 i 因此 scope chain 就會觸發 1. 也就會去尋找它的 outer reference 也就是存在 buildFunctions 函式內部的 i = 3 1. 因此印出結果是 3 1. 執行結束後 EC 跳出 2. 執行 `fs[1]()` 3. 以下以此類推...... 4. 由於他們的 outer reference 都是相同的因此印出的結果都是 3 ![](https://i.imgur.com/EhkZnPq.png) ## 如果不想要 Closures 發生呢 ### ES6 解法 ```javascript= function buildFunctions2() { arr = []; for (var i = 0; i < 3; i++) { let j = i; arr.push(function () { console.log(j); }); } return arr; } var fs2 = buildFunctions2(); fs2[0](); //0 fs2[1](); //1 fs2[2](); //2 ``` 使用 ES6 的語法 let 來操作: 因為 let 的作用域在大括號內,所以每次 for loop 執行時,都會有一個新的變數 j 存在記憶體中,也就會保留不同的 i 的值 ### ES5解法 ```javascript= function buildFunctions2() { arr = []; for (var i = 0; i < 3; i++) { arr.push( (function (j) { return function () { console.log(j); } }(i)) ) } return arr; } var fs2 = buildFunctions2(); fs2[0](); //0 fs2[1](); //1 fs2[2](); //2 ``` 這時候的 arr 變成三個立即函式 ```javascript= arr = [ //this is the first function pushed. (function (j) { return function () { console.log(j); } }(i)), //this is the second function pushed. (function (j) { return function () { console.log(j); } }(i)), //this is the third function pushed. (function (j) { return function () { console.log(j); } }(i)) ]; ``` 因為 IIFEs 的關係(立即執行函式)一樣會產生三個不同的 EC 並且保留不同的 j 當作 `fs2[i]()` 的 outer reference 就可以達成印出結果不為 3 ## 結論 這邊就是 Closures 確保我們內部的函式可以取用到值當我們在最底部(`fs[i]()`)執行它時,再好好的觀看上面的程式範例確保有看懂,那就表示你能理解 JS 進階的程式觀念 # Framework Aside: Function Factories > 使用 closures 來為函式製造一些預設的變數 > 使用 closures 來製造 pattern 讓函式更方便 ```javascript= function makeGreeting(language){ return function (firstname, lastname){ if(language ==='en')} console.log('Hello', firstname+' '+lastname); } if(language ==='es'){ console.log('Hola', firstname+' '+lastname); } } var greetEnglish = makeGreeting('en'); var greetSpanish = makeGreeting('es'); greetEnglish('John','Doe'); greetSpanish('John','Doe'); ``` 從 greetEnglish, greetSpanih 的指派中可以理解,因為各呼叫了一次因此產生了兩個 EC 並且留住了兩個變數 en, es 給下面的 invoke 使用,雖然它們兩個在執行的時候都是同一個函式,但是因為個別的 EC 以及個別的記憶體空間內容,讓 closures 可以抓到不一樣的變數內容 於是我們執行得出的結果是 ``` Hello John Doe Hola 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 使用 ```javascript= function sayHiLater(){ var greeting = 'Hi!'; setTimeout(function(){ console.log(greeting); },3000); } sayHiLater(); ``` ## jQuery 在 click 部份一樣使用函式表達式把函式當作參數使用(first-class function 的一種特性) ```javascript= // jQuery uses function expression and first-class functions ! $('button').click(function(){ do something }) ``` ## callback functions > 將函式作為參數傳遞到另一個函式時,被當作參數傳遞的那個函式我們稱之為回調函式 Callback function ```javascript= function tellMeWhenDone(callback){ var a = 1000; var b = 2000; callback(); } tellMeWhenDone(function(){ console.log('I am done!'); }) tellMeWhenDone(function(){ console.log('All done...'); }) ``` # call(), apply() and bind() functions 是一種特殊的物件,包含 * name(可以匿名) * code 程式碼內容(可調用的 invocable) * call() * apply() * bind() ![](https://i.imgur.com/HCPJEFZ.png) ## bind() 範例 從下方程式碼中 `logName()` 是會報錯的,因為裡面的 this 指向全域但是全域中卻沒有getFullname 這個 method ,會出現 this.getfullname is not a function at logName ```javascript= var person ={ firstname:"John", lastname:"Doe", getFullName: function(){ var fullname = this.firstname + ' '+ this.lastname; return fullname } } var logName = function(lang1,lang2){ console.log('Loged:'+ this.getFullname()); } logName(); ``` 這時就可以使用 `bind()` 它會製造出一個複製的 function 並且讓括號內的參數變為 this 的指向 ### 方法一 把 logName 綁訂到 person 物件上面改變 this 的指向到 person 身上,就能正確印出 John Doe 搂 ! **注意**這邊 logName 沒有調用的緣故是把 function 當作物件使用 bind 這個方法,如果調用了則變成 logName 裡面的值則無法使用此方法 ```javascript= var logNameRights = logName.bind(person) logNameRights(); ``` ### 方法二 也可以直接使用在 logName 後面直接使用 `bind(person)` 也能正確印出 John Doe ```javascript= var logName = function (lang1, lang2) { console.log('Loged:' + this.getFullname()); }.bind(person); logName(); ``` ### 放入參數 即使是操作 `bind()` 後,也還是可以帶入參數並且正確印出內容 ```javascript= var logName = function (lang1, lang2) { console.log('Loged:' + this.getFullname()); console.log('Arguments:'+ ' '+ lang1+' '+lang2); console.log('--------------------------------'); }.bind(person); var logNameRights = logName.bind(person); logNameRights('en'); ``` ![](https://i.imgur.com/8er2Xuh.png) ## `call()` 範例 * 相較於 `bind()` 的複製一個 function 操作 * `call()` 則是直接執行函式 ```javascript= var logName = function (lang1, lang2) { console.log('Loged:' + this.getFullname()); console.log('Arguments:' + ' ' + lang1 + ' ' + lang2); console.log('--------------------------------'); }.bind(person); logName.call(person,'en','es'); ``` ## `apply()` 範例 基本上跟 `call()` 是一樣的操作方式只是,參數的部分必須以陣列的方式放入 ```javascript= var logName = function (lang1, lang2) { console.log('Loged:' + this.getFullname()); console.log('Arguments:' + ' ' + lang1 + ' ' + lang2); console.log('--------------------------------'); }.bind(person); logName.apply(person,['en','es']); ``` ## 也可以使用 IIFEs 操作 一樣可以印出結果 ```javascript= (function (lang1, lang2) { console.log('Loged:' + this.getFullname()); console.log('Arguments:' + ' ' + lang1 + ' ' + lang2); console.log('--------------------------------'); }).apply(person, person, ['en', 'es']); ``` ## 實際應用 ### function borrowing 借用 function getFullName 從 person ,用來印出 person2 的全名 Jane Doe ```javascript= var person2 = { firstname:"Jane", lastname:"Doe", } console.log(person.getFullName.apply(person2)); // 當然也可以用 call ``` ### function curring (只能操作在 bind) > 創造一個 copy 的函式並且有著固定的預設參數 已知 bind 內的第一個參數會是 this 的指向 之後的參數則為使用函式的固定參數 以下面程式碼為例: ```javascript= function multiply(a, b) { return a * b; } var multiplyByTwo = multiply.bind(this, 2); console.log(multiplyByTwo(4)); var multiplyByThree = multiply.bind(this, 3); console.log(multiplyByThree(4)) ``` `bind(this,2)` ```javascript= function multiply(a, b) { return a * b; } ``` 則 a 為 2 `bind(this,3)` ```javascript= function multiply(a, b) { return a * b; } ``` 則 a 為 3 藉由這樣的方式固定操作函式的參數讓其固定就被稱作 function curring # Functional Programming > First class function 表示 functions 表現得跟物件一樣,可以當作參數傳遞,可以 return 一個 function 也就意味著我們可以執行所謂的 functional programming > 把重複的程式碼拆出來作為參數使用,減少重複撰寫程式碼的方法就是 **functional programming** ## 讓我們看看下方程式碼範例: 這邊是比較攏長的版本 ```javascript= var arr1 =[1,2,3]; console.log(arr1); // [1,2,3] var arr2 = []; for(var i =0; i<arr1.legnth;i++){ arr2.push(arr1[i]*2); } console.log(arr2); //[2,4,6] ``` 我們要寫進 function 裡面讓程式碼被分段開來以便修改裡面的邏輯之外也更好重複利用重複的部分 所以我們這樣寫 從下方程式碼可以理解到,我們已經把 for loop 整個拉出來變成 mapForEach function 了,用來把 arr1 推進去 new array 裡面,再放到 arr2 裡面去做操作 ```javascript= function mapForEach(arr,fn){ var newArr =[]; for(var i =0; i<arr.legnth;i++){ newArr.push( fn(arr[i]) ) }; return newArr; } var arr1 = [1,2,3]; var arr2 = mapForEach(arr1,function(item){ return item*2; }) console.log(arr2); // [2,4,6] ``` 因此當我們想要繼續這個功能時我們可以這樣寫 已經不需要再重複 for loop 的部分因為已經被我們分段提取出來重複利用了 ```javascript= var arr3 = mapForEach(arr1,function(item){ return item * 3 }) console.log(arr3) //[3,6,9] ``` 甚至可以改變其中的邏輯 ```javascript= var arr4 = mapForEach(arr1,function(item){ return item > 2 }) console.log(arr4) //[false,false,true] ``` 我們就可以針對 arr1 去做不一樣的操作並且不需要重複撰寫 for loop 的部分,只要引入 marForEach function 就好,這樣把重複的程式碼拆開來使用減少重複撰寫的方法就是 **functional programming** ## 下一個範例 這邊傳入的 function checkPastLimit 需要擺入兩個參數才能操作所以,用到上面教到的 bind 的方法給於參數做預設值 [`bind(this, 後面就是預設值)`](#function-curring-只能操作在-bind) 就可以正常操作 arr1 來比較是否大於 1 ,而得出 [false,true,true] 這個結果 ```javascript= var checkPastLimit = function(limiter,item){ return item > limiter; } var arr4 = mapForEach(arr1, checkPastLimit.bind(this,1)); console.log(arr4)//[false,true,true] ``` ### 覺得使用 .bind 太麻煩了 ? 那就再把那個 function 丟到另一個 function 內,在那個 function 操作 .bind 即可 ```javascript= var checkPastLimitSimplified = function(limiter){ return function(limiter,item){ return item > limiter; }.bind(this,limiter) } var arr5 = mapForEach(arr1, checkPastLimitSimplified(2)); console.log(arr5) // [false,false,true] ``` 我們可以利用這樣的方式讓最後呈現出來的程式碼非常簡潔易讀並且容易重複利用或是改寫,因為把重複的部分提取出來並且透過參數的方式傳入其他 function 做操作,這就是 functional programming 強大的地方 ## 注意 因為 function 傳遞到很多地方,所以盡量避免修改裡面的值,減少出錯的機率,或是真的要做修改盡量在整個 function 傳遞鍊比較高層的地方就修改好,就像是這邊範例中的 newArr 再一開始就做好修改的動作,這就是傳遞鍊比較高層的意思 # Functional Programming Part 2 這篇主要介紹 [underscore.js](https://underscorejs.org/#map)這個函式庫 作者希望我們可以觀看裡面的程式碼去做學習,學習其中的 functional programming 的方式以及把程式碼寫得更簡潔易懂 可以在此處取得 [CDN](https://underscorejs.org/#map) 後直接載入使用此函式庫 ```html= <body> <script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script> <script src="app.js"></script> </body> ``` 簡單的使用 `(_)` 並且加上 API 名稱就可以使用此函式庫了 ```javascript= var arr1 = [1, 2, 3] var arr6 = _.map(arr1, function (item) { return item * 3 }) console.log(arr6); ```