--- title: 'JS 核心 15 - 函式、立即函式' tags: JS 核心 ,JS , JavaScript, this description: 2021/02/11 --- JS 核心 -- 函式、立即函式 === ## 什麼是函式 透過 function 宣告一個函式,此函式所宣告出來的物件跟一般的物件有些不同。 1. 被呼叫的能力 : 在函式名稱後方加上括號(用來傳參數使用),此時多了被呼叫的能力。在呼叫函式的同時,也可以把參數傳進來。 2. 包含程式碼的片段 : 函式的 { } 裡的內容是程式碼的片段,也是函式所擁有,一般物件並沒有包含這段。 3. 有回傳的功能 : 函式可以 return 回傳一段值 (可以是純值或物件) :pencil2: **使用 function 關鍵字宣告字詞時,此字詞就會變成一段函式,此函式就包含以下能力。** ![](https://i.imgur.com/t4FxY8F.png) ``` function afunction(parameter) { var localVariable = '區域變數'; console.log(this, localVariable); // this (如下圖)、區域變數 return '附加一段' + parameter; // 回傳參數內容 } var data = afunction('參數'); // 這段是表達式,會回傳一個值,此值再賦予到變數 data 上 console.log(data); // 附加一段參數 (data 的值來自於函式的回傳) ``` window 是此 function 的 this 所呈現的值 ![](https://i.imgur.com/pNNog7U.png) ## 函式陳述式 (又稱具名函式) 1. 透過「函式陳述式」的方式來建立一段函式 2. 直接透過 function 來宣告一個函式 3. <span class="red">**要求一定要補上名稱,否則無法呼叫。此具有名稱的函式又稱為「具名函式」**</span> ``` function functionA() { // 使用 function 關鍵字來定義一段函式 console.log('函式陳述式', '具名函式'); console.log(functionA); } functionA(); // 呼叫此段函式 ``` ## 函式表達式 (又稱匿名函式) 1. 先宣告一個變數 2. 使用 function 關鍵字來定義一段函式的物件 (此函式沒有名稱) 3. 這段函式物件透過「等號運算子」賦予到 functionB 的變數上 4. 變數 functionB 接收的是此段函式的參考路徑,這參考路徑下的函式是沒有名稱的 (又稱匿名函式) 5. 此段函式物件可以被呼叫 6. 不是所有的「函式表達式」都是「匿名函式」 ``` var functionB = function() { // 使用 function 關鍵字來定義一段函式 console.log('函式表達式', '匿名函式'); console.log(functionB); } functionB(); // 執行此段函式 ``` ## 「具名函式」和「匿名函式」的名稱在哪裡呢 ? ![](https://i.imgur.com/YMg7HRC.png) ### 「具名函式」: * functionA 來自函式的片段 * 在 " f " 後面可以看到 functionA 這個名稱,裡面也包含程式碼的片段 ### 「匿名函式」: * 在 " f " 後面沒有名稱,裡面直接帶入程式碼的片段 ## 函式表達式中的具名函式 ### :pencil2:「具名函式」可以在函式內被調用 ```typescript= var functionC = function functionD() { console.log(functionC, functionD); // 具名函式 functionD 能夠在函式內被調用 } functionC(); // 執行這段函式 ``` 1. 先宣告 functionC 的變數,後面帶入另一段函式的表達式,這段函式宣告有給一個名稱 functionD 2. functionD 是函式的名稱,functionD 此「具名函式」可以在函式內被調用 3. (如下圖) " f " 後面出現 functionD 的名稱。雖然宣告的變數是 functionC ,但實際上函式的名稱是 functionD,所以 console.log(functionC, functionD); 印出的結果都是顯示 functionD 這個名稱。因為 functionD 是函式的名稱,並非指變數。 ![](https://i.imgur.com/lFe5WfW.png) 若把 console.log(functionC, functionD); 貼到外層就有不一樣的結果。 * **functionC 在外層可以執行,變數 functionC 是接收函式 functionD 的內容 (接收 functionD 的參考位置)。** * <span class="red">**functionD 只能在函式內被調用,在外層是取不到 functionD 這個名稱的。**</span> ```typescript=5 console.log(functionC); // 可以執行,變數 functionC 是接收函式 functionD 的內容 (接收 functionD 的參考) console.log(functionD); // functionD is not defined (在外層取不到 functionD) ``` ### :pencil2: 範例 1. 先宣告 giveMeMoney 的變數,後面帶入一段 giveMoreMoney 函式 2. giveMoreMoney 是具名函式的名稱,並非為 giveMeMoney 的函式 (具名函式可在函式內被調用) ``` var num = 1; var giveMeMoney = function giveMoreMoney(coin) { num += 1; console.log('執行 giveMeMoney', num, coin); return coin > 100 ? coin : giveMoreMoney(num * coin); // 判斷式,當傳入金額不夠時,就試著增加金額 (觸發 giveMoreMoney 函式),直至超過 100 元才會停止 }; console.log(giveMeMoney(30)); // (結果如下左圖) ``` > <span class="red">函式到底需不需要名稱在於是否可被調用 > 如果可以被調用,那名稱可以被省略</span> <span class="green">以上例來說直接執行 giveMeMoney 是沒有問題的,那 giveMoreMoney 名稱就可以被省略。</span> (寫法如下) ``` var num = 1; var giveMeMoney = function(coin) { num += 1; console.log('執行 giveMeMoney', num, coin); return coin > 100 ? coin : giveMeMoney(num * coin); }; console.log(giveMeMoney(30)); // (結果如下左圖) ``` ![](https://i.imgur.com/6M5tkVJ.png) ### :pencil2: 函式陳述式 (具名函式) 若沒有名稱,則無法執行 ``` function callSomeone(fn) { fn(); // 執行函式 (一定要傳入一個函式,才能執行) console.log(fn); // ƒ () { console.log('執行函式') } } callSomeone(function() { console.log('執行函式') }); // (結果如上右圖) // 函式陳述式 (具名函式) 一定要有名稱才能被呼叫執行 // 傳入 callSomeone 的參數為一段匿名函式(不需要名稱),因為他傳入變成參數時就如同函式表達式。 // 定義一段函式並賦予到參數 fn 上,參數 fn 接受了此匿名函式。 ``` --- ## 立即函式 IIFE 1. 不需要呼叫此函式,也能執行 * 立刻執行 2. IIFE 最主要目的是避免污染到全域執行環境並照成污染與衝突 * 限制作用域的用途 (連同函式宣告都能限制作用域) * 變數的作用域只在函式內,在外層無法取得變數 * 「立即函式」無法在外層呼叫 3. 「立即函式」本身是表達式,所以也能回傳一個值 * 立即可以被呼叫的「函式表達式」,定義完即可執行 ### :pencil2: 範例 : 透過「具名函式」的方式來執行的 IIFE,且 IIFE 無法在外層被呼叫 ``` (function IIFE() { console.log('立即函式', IIFE); }()); console.log(IIFE); // IIFE is not defined (無法在函式外被再次執行) IIFE; // IIFE is not defined (「立即函式」無法在外層被呼叫) ``` ![](https://i.imgur.com/7dsWWer.png) ### :pencil2: 範例 : IIFE 也是可自我執行的「匿名函式」 ``` (function () { console.log('立即函式'); // 立即函式 }()); ``` 小括號的位置也可移至外層 ``` (function () { console.log('立即函式'); // 立即函式 })(); ``` ### :pencil2: 範例 : 變數只活在 IIFE 內 > 「立即函式」可以避免裡面的變數污染到 global scope。 變數 Ming 宣告在「立即函式」的內層,只能在「立即函式」內被取得,在外層無法取得變數 ``` (function() { var Ming = '小明'; // 透過「立即函式」限制變數的作用域 console.log(Ming); // 小明 })(); console.log(Ming); // Ming is not defined (在外層無法取得變數) ``` ### :pencil2: 範例 : 限制作用域的用途 > 就是為了避免命名衝突,而範例中 getName 是屬於全域的函式。因此在開發中,getName 還是可能與其它變數名稱衝突,如果透過立即函式,就連同函式宣告都能限制作用域。 ``` function getName() { var Ming = ‘小明’; console.log(Ming); // (限制變數的作用域) }; getName(); // 小明 console.log(Ming); // Ming is not defined ``` ### :pencil2: 範例 : 「立即函式」也能傳遞參數 「立即函式」本身是表達式,所以也能回傳一個值 > 可以利用 return 並搭配一個變數來接收 IIFE ``` var whereMing = (function (where) { console.log(where); return where; // return 會把值傳出來外層,外層變數就可接收此函式 })('小明在這'); // 透過 "小括號" 把參數往前傳 console.log(whereMing); // 小明在這 (變數 whereMing 接收此函式的 return 結果) ``` ### <span class="green">return用法 : </span> > * 只要你需要將結果回傳時,就會使用到該方式。 > * 這邊主要觀念與執行堆疊的記憶體釋放有關係,當函式執行完畢後就會釋放記憶體,所以若要保留結果,那麼就必須 return。 ### :pencil2: 範例 : 「立即函式」不符合 ASI 規則,無法自動插入分號 以下兩個「立即函式」因沒有使用分號隔開被視為同一行 「立即函式」不符合 ASI 規則,無法自動插入分號 ``` (function(){ })() // 跳錯,(intermediate value)(...) is not a function (function(){ })() // 這個括號的內容不是 function ``` 解決方法 : 在使用任何「立即函式」的**前方或後方**要補上分號,才能正確執行 ``` (function(){ })(); (function(){ })() ``` ### :pencil2: 範例 : 把前一個「立即函式」的內容傳至下一個「立即函式」內 「立即函式」傳遞變數的手法 * **使用物件 "傳參考" 的特性傳遞** ``` var a = {}; // 先宣告一個物件,物件有傳參考的特性 (function(b){ b.person = '小明'; console.log(b === a); // true console.log(b.person === a.person); // true })(a); (function(c){ console.log(c.person); // 小明 console.log(c === a); // true console.log(c.person === a.person); // true })(a) ``` 1. function(b) 和 function(C) 裡面的 b 和 c 參數,都是傳進來 a 的值。 如果將 a 與 b 和 c 比較會發現都為 true。 a 與參數 b、c 的值是一樣的。 2. 有一個 a 的物件帶到b,又因為物件是傳參考,所以 a 和 b 的參考路徑都是相同,所以 b.person=小明; 實際上因參考路徑相同所以 a、b 底下都有一個屬性 person 跟值小明,第二個 立即函式也接收了 a 的參考路徑故 console.log(c.person); //小明 。 a、b、c的參考路徑都是相 同。 * **將參數掛載到 window 全域物件的方式傳遞** > 將 window 傳入,可以確保框架正確的掛載到全域變數 window 上 > ``` (function(global) { global.person = '小明'; })(window) // window 是全域物件,把 window 全域物件傳到前面這個「立即函式」 ;(function(c) { console.log(person); // 小明 })() ``` 1. 將傳進第一個立即函式的參數改為全域物件 window。第二個立即函式印出全域物件的屬性 person。 2. window 是一個物件,而透過 global 傳入後也是一個物件傳參考特性,因此 global.person 其實就是 window.person。 3. window 是全域物件,把 window 全域物件傳到前面的立即函式,在全域物件掛上屬性 person,並把值傳到另一個立即函式。 ``` var greeting = 'Hola'; (function(global, name){ var greeting = 'Hello'; global.greeting = 'Hello' // 讓 Hola 變成 Hello console.log(greeting+' '+name) // Hello Iris })(window, 'Iris') // 取用全域中的變數,並代入 IIFE 中來更改全域變數 console.log(greeting) // Hello ``` ### :pencil2: 練習 : 執行環境、物件參考觀念 ``` var vm = window; var food = '水牛城雞翅'; window.food = '水牛城雞翅123'; console.log(food); // food = 水牛城雞翅123 console.log(window.food); // food = 水牛城雞翅123 (function(global){ var food = '雞塊'; // 此變數 food 僅存活於立即函式內 global.food = '麥脆雞'; console.log(global === vm); // true console.log(global.food === food); // false console.log(food); // food=雞塊 console.log(global.food); // food=麥脆雞 })(window); console.log(food); // food=麥脆雞 ``` 1. var 在立即函式內宣告 (以函式當作作用域) 2. 首先 window 本身是一個物件,所以你透過函式參數傳入後其實是屬於物件參考的傳入,也因此才會導致 global.food = '麥脆雞'; 影響到外層的 window。 ## :memo: 學習回顧 :::info * 函式陳述式 (又稱具名函式) * 這種函式不會回傳任何值,只是執行了一些行為,而且會被提升 * <span class="red">**要求一定要補上名稱,否則無法呼叫。**</span> * <span class="red">**「具名函式」可以在函式內被調用,在外層是取不到此函式 → 如果可以被調用,那名稱可以省略。**</span> * 函式表達式 (又稱匿名函式) * 這邊宣告的變數會被提升 (hoisting),函式不會 * 此段函式物件透過「等號運算子」賦予到 functionB 的變數上 * 此段函式物件可以被呼叫 * <span class="red">**不是所有的「函式表達式」都是「匿名函式」**</span> * 「立即函式」本身是表達式,所以能回傳一個值 * IIFE 最主要目的是避免污染到全域執行環境並照成污染與衝突 * 限制作用域的用途 (連同函式宣告都能限制作用域) * 變數的作用域只在函式內,在外層無法取得變數 * 「立即函式」無法在外層呼叫 * 立即可以被呼叫的「函式表達式」,定義完即可執行 * 「立即函式」不符合 ASI 規則,無法自動插入分號 ::: ## :+1: 相關參考文件 :::info [Ray - 函式以及 This 的運作-立即函式](https://hsiangfeng.github.io/javascript/20201118/707576253/) [Ray - 函式](https://w3c.hexschool.com/blog/cb6e361) [Kuro - 函式 Functions 的基本概念](https://ithelp.ithome.com.tw/articles/10191549) ::: <style> .red { color: red; } .green { color: green; } </style>