iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 19
0
自我挑戰組

學JS的心路歷程系列 第 19

學JS的心路歷程 Day19-閉包 closure

  • 分享至 

  • xImage
  •  

閉包是是純函式語言的一個特性,也是 JS 的一個關鍵性的特色,雖然不了解也能開發程式,但我們不是這種人對吧?

閉包不僅可以減少某些高階功能的程式碼數量和複雜度,並且可以讓我們做到原本無法做的複雜功能。聽到這還不想認識他嗎!

那什麼是閉包呢?它是一種資料結構,可以說是一種技術,能記住函式及函式被建立時當下環境,也就是說函式可以存取在建立時作用範圍內的變數。

我們先來看一個最簡單的範例:

var outer = "global";
function outerFun(){
    console.log(outer);
}
outerFun();//"global"

你可能會疑問這是閉包嗎?平常就是這樣用啊!

我們在同一個作用範圍(也就是全域)中宣告一個變數 outer 和函式 outerFun ,並呼叫函式。
這個範圍就是一個閉包,只是在程式結束時永遠不會消失,讓你沒有感覺而已。

在 JS 中每當函式被建立時,一個閉包就會產生。雖然很多人經常拿 巢狀函式來作為範例來說明閉包,但並不是只有它才能產生,要記住這點!

雖然這麼說,但我們還是會以巢狀函式來舉例,感覺有點打自己臉....

function outerFun(x){
    function innerFun(){
		x++
		console.log(x);
    }
    return innerFun;
}
var num = outerFun(1);
num();//2
num();//3

在執行後會發現我的 x 怎麼每一次執行都會在加一呢?就是因為閉包造成的。

它們在函式建立時為函式及作用範圍內的變數建立了一個類似「安全氣泡」的東西,讓我們在執行時可以使用。

剛剛的程式碼,還可以簡短成這樣:

function outerFun(x){
    return ()=>{
        x++;
        console.log(x);
    }
}
var num = outerFun(1);
num();//2
num();//3

不過這邊有幾點要先各位說明,避免產生誤會:

  • 函式呼叫 和 函式建立 是兩回事,在 outerFun 函式呼叫後,num 才是 函式建立,這時候 num 的閉包結構才會產生
  • 閉包不是只會產生在巢狀(內部)函式的回傳時。所有函式在建立時都會產生閉包。

我們也可以透過下中斷點的方式觀察閉包中的值:

所以我到底可以用哪些外部變數呢?基本上一個內部函式可以有三個作用域:

  • 自已本身的
  • 外部函式的
  • 全域的

內部函式可以看到(或存取)外部函式,而形成一個Scope Chain(作用域連鎖)。

這邊我們來舉一個實際的例子:
我們先建立兩個按鈕和一個顯示區

<button id="btn">Click one</button>
<button id="btn2">Click two</button>
<div id="display"></div>

然後寫一個計數器的函式,並掛上監聽事件

var btn = document.getElementById('btn');
var btn2 = document.getElementById('btn2');
var display = document.getElementById('display');

function recordClick (name){
  let count = 0;
  return ()=>{
    count++;
    console.log(count);
    display.innerHTML = `${name} count : ${count}`;
  }
}

btn.addEventListener("click",recordClick("Click one"));
btn2.addEventListener("click",recordClick("Click two"));

執行後可以發現到兩個按鈕並不會有共用一個 count 問題,這是因為利用閉包產生了一個類似私有變數的功能。
這邊附上 codepen範例 給大家玩玩。

那這邊可能大家會有一個問題,到底閉包是複製了這些值還是只是參照而已呢?
答案是參照,最常拿來解說的就是利用 非同步回呼函式來舉例:

function counter() {
    let i = 0;
    for (i = 0; i< 5; i++) {
        setTimeout(function() {
            console.log('counter is ' + i)
        }, 1000);
    }
}
counter();

可能很多人認為說結果會是 1,2,3,4,5 但是實際印出來怎麼都是 5?

這是因為「閉包結構中所記憶的環境值是用參照指向的」, setTimeout 會先到佇列中準備延遲執行,等到回來主程式時,迴圈早就執行完了,i 也已經變成了 5 ,要解決這個問題其實可以這樣解:

function timer(index){
    return setTimeout(function() {
        console.log('counter is ' + index)
    }, 1000);
}
function counter(){
    let i = 0;
    for (i = 0; i< 5; i++) {
        timer(i);
    }
}
counter();
//counter is 0
//counter is 1
//counter is 2
//counter is 3
//counter is 4

創建一個函式 timer 讓迴圈中的 i 值傳入並形成一個閉包,因為每次傳入 timer 的值都不同,所以每個 setTimeout 閉包中的值也會不同。

我們也可以這樣解:

function showIndex(index){
    return ()=> console.log(index);
}
function counter(){
    let i = 0;
    for (i = 0; i< 5; i++) {
        setTimeout(showIndex(i), 1000);
    }
}
counter();

那麼,以上就是閉包的用法,一樣如果有錯誤及來源未附上也歡迎留言指正,那麼我們明天見。

參考資料 :
你所不知道的 JS 非同步處理與效能
從ES6開始的JavaScript學習生活
忍者 JavaScript 開發技巧探秘


上一篇
學JS的心路歷程 Day18-Promise(三)
下一篇
學JS的心路歷程 Day20-JS 支援物件導向?(一)
系列文
學JS的心路歷程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言