前端基礎

-

JavaScript 面試題 - 超好用的立即執行函數 IIFE

this.web

立即執行函數又稱 IIFE(Immediately Invoked Function Expression),對於初學者來說,很容易把立即執行函數跟閉包搞混,也很多文章會將它們兩個混在一起說,因為立即執行函數常常會和閉包結合起來使用。

如果你還不知道閉包是甚麼,可以參考我寫的這篇文章 JavaScript 面試題 - 閉包 Closure 的觀念及實作。裡面用很簡單的一句話來說明 JS 閉包,並且有三個實作範例。

那接下來就讓我們來看看立即執行函數是什麼,以及他是怎麼跟閉包結合的吧!整篇文章的架構為:

  1. 立即執行函數是什麼
  2. 立即執行函數可以做什麼
  3. 立即執行函數的應用

立即執行函數 IIFE 是什麼

簡單說,立即執行函數就是在宣告完函數後直接使用,使用方法為:

  1. 用一個括號把函數刮起來後
  2. 馬上接一個小括號
(function () {
  console.log('IIFE');
})();
// IIFE

當然 IIFE 也可以使用箭頭函數 👇

(() => {
  console.log('IIFE');
})();
// IIFE

以上兩個寫法等於

function fn() {
  console.log('IIFE');
}

// 宣告完馬上執行
fn(); // IIEF

而 IIFE 立即執行函數也可以帶入參數使用:

(function (message) {
  console.log(message);
})('hi');
// hi

立即執行函數 IIFE 可以做什麼?

若只是宣告完馬上使用使用函數,顯然沒有太大用處。它最厲害的地方其實是用來模擬區塊作用域,因為在 ES6 出現 letconst 以前,是沒有區塊作用域的,這在某些時候非常不方便,所以我們會使用立即執行函數來模擬區塊作用域,舉例來說:

for (var i = 0; i < 1; i++) {
  console.log(i); // 0 1
}

console.log(i); // 1

若是用 var 宣告變數,則在 for 迴圈外部仍然可以讀取到變數 i。這不是一件好事,因為這樣容易覆蓋到變數的值,所以我們可以用 IIFE 立即執行函數來解決 👇

(function () {
  for (var i = 0; i < 1; i++) {
    console.log(i); // 0 1
  }
})();
console.log(i); // 錯誤

不過現在比較少看到這種寫法了,是因為 ES6 出現了 letconst,這裡個 key word 可以解決這個問題,也是我們平常用的寫法:

for (let i = 0; i < 1; i++) {
  console.log(i); // 0 1
}
console.log(i); // 錯誤

立即執行函數 IIFE 的應用 1 - 和閉包結合創造臨時獨立作用域

很多文章會將立即執行函數和閉包一起講是因為,他跟閉包結合可以創建臨時獨立作用域,例如我們想要創造一個計時器,但又不希望計時器裡面的變數 count 影響到外部,就可以如下面這樣寫:

// 設置一個每秒執行一次的定時器
setInterval(
  (function () {
    // 定義一個 IIFE
    let time = 0;
    return function () {
      // 利用閉包來保留 time 變數的值,並在每次執行時增加 1
      console.log(++time);
    };
  })(),
  1000,
);

// 不會和下面互相影響
let time = 100;
console.log(time); // 100

// 開始計時
// 1
// 2 ...

像是上面這樣,每一秒計時一次,而內部的 time 又不會跟外部的變數互相影響。

立即執行函數 IIFE 的應用 2 - 解決變數名衝突

有些時候我們引入的函式庫,可能會有變數名稱衝突的問題,舉例來說 jQuery 是用 $ 當作變數名稱,如果其他函式庫也是用 $,就會有衝突,此時可以傳入參數給立即執行函數來解決這個問題:

// 假設其他函式庫占用 $
const $ = () => console.log('Not jQuery');

(function ($) {
  // 通過閉包限制作用域的變數名稱
  $(document).ready(function () {
    console.log('Hello jQuery');
  });
})(jQuery);
// Hello jQuery

$(); // Not jQuery

以及在閉包貼文中提到的函式庫封裝,也是用立即執行函數來避免變數名稱的衝突:

(function () {
  var jQuery = (window.$ = function () {
    // Intialize
  });
})();

立即執行函數 IIFE 的應用 3 - 製作具有私有變數的物件

在 JS 中,沒有私有變數的概念,但我們可以透過立即執行函數創造獨立作用域來模擬私有變數:

const module = (function () {
  let privateVar = '私有變數';

  function privateFunction() {
    console.log('這是私有函數');
  }

  return {
    publicVar: '公開變數',
    publicFunction: function () {
      console.log('這是公開函數');
      console.log('可以訪問私有變數:' + privateVar);
      privateFunction();
    },
  };
})();

module.publicFunction(); // 輸出:這是公開函式、可以訪問私有變數:私有變數、這是私有函數

console.log(module.publicVar); // 輸出:公開變數
console.log(module.privateVar); // 輸出:undefined
module.privateFunction(); // 輸出:錯誤,因為 privateFunction 是私有函式

立即執行函數 IIFE 的應用 4 - 讓 if else 更簡潔

有時候我們會利用 if else 來處理變數的值,比如說:

let animalName = 'rabbit'; // 預設動物是兔子
if (pet.canBark()) { // 如果動物會吠
  if (PerformanceTiming.isScary()) animalName = 'wolf'; // 如果動物很可怕
  else animalName = 'dog'; 
} else if (pet.canMeow()) { // 如果動物是喵喵叫
  animalName = 'cat';
}

這樣有兩個壞處,一是有巢狀的 if else 不易閱讀,二是當程式碼一多的時候,我們在後面的程式碼可能會不小心改到 animalName。

我們可以搭配 const 和立即執行函數來優化這段程式碼👇

const animalName = (() => {
  if (pet.canBark() && pet.isScary()) return 'wolf';
  if (pet.canBark()) return 'dog';
  if (pet.canMeow()) return 'cat';
  return 'rabbit';
})

利用 const 就可以避免之後的程式碼更動到 animalName 的變數值。

小結和後記

今天我們介紹了立即執行函數,簡單說就是宣告完函數後馬上使用,它最主要的應用有四個

  1. 模擬塊級作用域
  2. 避免相同變數名的衝突
  3. 製作有私有變數的物件
  4. 優化 if else 的寫法

所以它並不只是單純馬上使用函數那麼簡單,雖然因為 ES6 新增 class 類語法,大大減少了 IIFE 和閉包的使用率,不過很多舊的程式碼仍然是使用 IIFE 和閉包,所以它還是很重要的觀念喔!

後記

在我寫了更多程式有更多經驗後,發現在 React 中,可以很常使用立即執行函數的第四種應用來優化程式碼,例如:

const SomeComponent = () => {
  const someValue = (() => {
    // 執行某些判斷
  })

  return <div>{someValue}</div>
}

這樣可以讓程式碼變得很簡潔。所以說 IIFE 還是很重要且實用的觀念~

相關系列文章