1. 程式人生 > 程式設計 >JavaScript閉包原理及作用詳解

JavaScript閉包原理及作用詳解

目錄
  • 簡介
  • 閉包的用途
    • 柯里化
    • 實現公有變數
    • 快取
    • 封裝(屬性私有化)
  • 閉包的原理
    • 垃圾收集
      • 簡介
      • 實際開發中的優化

    簡介

    說明

    本文介紹的閉包的作用、用途及其原理。

    閉包的定義

    閉包是指內部函式總是可以訪問其所在的外部函式中宣告的變數和引數,即使在其外部函

    數被返回(壽命終結)了之後。

    閉包的作用(特點)

    1.函式巢狀函式

    2.內部函式可以引用外部函式的引數或者變數

    3.外部函式的引數和變數不會被垃圾回收,因為被內部函式引用。

    閉包與全域性變數

    JavaScript閉包原理及作用詳解

    閉包的用途

    柯里化

    可以通過引數來生成不同的函式。

    function makeWelcome(x) {
    	return function(y) {
    		return x + y;
    	};
    }
     
    let sayHello = makeWelcome("Hello,");
    let sayHi = makeWelcome("Hi,");
     
    console.log(sayHello("Tony"));
    console.log(sayHi("Tony"));

    結果

    Hello,Tony

    Hi,Tony

    實現公有變數

    需求:實現一個累加器,每次呼叫就增加一次。

    function makeCounter(){
    	let count = 0;
    	function innerFunction(){
    		return count++;
    	}
    	return innerFunction;
    }
    let counter = makeCounter();
     
    console.log(counter());
    console.log(counter());
    console.log(counter());

    結果

    0

    1

    2

    快取

    設想有一個處理過程很耗時的函式物件,可以將計算出來的值儲存起來,當呼叫這個函式的時候,首先在快取中查詢。如果找不到,則進行計算,然後更新快取並返回值;如果找到了,直接返回查詢到的值即可。

    閉包可以做到這一點,因為它不會釋放外部的引用,從而函式內部的值可以得以保留。

    本處為了簡單,直接寫讀寫快取的示例。(而不是讀不到再計算,然後存到快取)。

    let cache = function () {
    	// Map允許鍵為任意型別。如果這麼寫:let storage = {},則鍵只能為字串
    	let storage = new Map();
    	return {
    		setCache: function (k,v) {
    			storage[k] = v;
    		},getCache: function (k) {
    			return storage[k];
    		},deleteCache: function (k) {
    			delete storage[k];
    		}
    	}
    }();
     
    cache.setCache('a',1);
    console.log(cache.getCache('a'))

    結果

    1

    封裝(屬性私有化)

    只能通過提供的閉包的形式來訪問內部變數。(此法不好,建議使用原型鏈)。

    let person = function(){
    	//變數作用域為函式內部,外部無法訪問  
    	let name = "www.cppcns.comdefaultName";
     
    	return {
    		getName: function(){
    			return name;
    		},setName: function(newName){
    			name = newName;
    		}
    	}
    }();
     
    console.log(person.name);
    console.log(person.getName());
    person.setName("Hello");
    console.log(person.getName());

    結果

    undefined

    defaultName

    Hello

    閉包的原理

    以計數器為例:

    function makeCounter() {
    	let count = 0;
    	return function() {
    		return count++;
    	};
    }
    let counter = makeCounter();
    console.log(counter());
    console.log(counter());
    console.log(counter());

    結果

    0

    1

    2

    每次 makeCounter() 呼叫的開始,都會建立一個新的詞法環境物件,以儲存該makeCounter 執行時的變數。

    因此,我們有兩層巢狀的詞法環境:

    JavaScript閉包原理及作用詳解

    在執行 makeCounter() 的過程中建立了一個僅佔一行的巢狀函式: return count++ 。我們尚未執行它,僅建立了它。

    所有的函式在“誕生”時都會記住建立它們的詞法環境。原理:所有函式都有名為 [[Environment]] 的隱藏屬性,該屬性儲存了對建立該函式的詞法環境的引用:

    JavaScript閉包原理及作用詳解

    因此, counter.[[Environment]] 有對 {count: 0} 詞法環境的引用。這就是函式記住它創建於何處的方式,與函式被在哪兒呼叫無關。 [[Environment]] 引用在函式建立時被設定並永久儲存。

    稍後,當呼叫 counter() 時,會為該呼叫建立一個新的詞法環境,並且其外部詞法環境引用獲取於 counter.[[Environment]] :

    JavaScript閉包原理及作用詳解

    現在,當 counter() 中的程式碼查詢 count 變數時,它首先搜尋自己的詞法環境(為空,因為那裡沒有區域性變數),然後是外部 makeCounter() 的詞法環境,並且在哪裡找到就在哪裡修

    改(在變數所在的詞法環境中更新變數)。

    這是執行後的狀態:

    JavaScript閉包原理及作用詳解

    如果我們呼叫 counter() 多次, count 變數將在同一位置增加到 2, 3等。

    垃圾收集

    簡介

    通常,函式呼叫完成後,會將詞法環境和其中的所有變數從記憶體中刪除,因為現在沒有任何對它們的引用了。

    與 Script 中的任何其他物件一樣,詞法環境僅在可達時才會被保留在記憶體中。但是,如果有一個巢狀函式在函式結束後仍可達,則它具有引用詞法環境的[[Environment]] 屬性。

    如果在函式執行完成後,詞法環境仍然可達,則此巢狀函式仍然有效。例如:

    function f() {
        let value = 123;
        return function() {
            alert(value);
        }
    }
    // g.[[Environment]] 儲存了對相應 f() 呼叫的詞法環境的引用
    let g = f();

    如果多次呼叫 f() ,並且返回的函式被儲存,那麼所有相應的詞法環境物件也會保留在記憶體中。例如:

    function f() {
        let value = Math.random();
        return function () {
            alert(value);
        };
    }
     
    // 陣列中的 3 個函式,每個都與來自對應的 f() 的詞法環境相關聯
    let arr = [f(),f(),f()];

    當詞法環境物件變得不可達時,它就會死去(就像其他任何物件一樣)。換句話說,它僅在至少有一個巢狀函式引用它時才存在。

    在下面的程式碼中,巢狀函式被刪除後,其封閉的詞法環境(以及其中的 value )也會被從記憶體中刪除:

    function f() {
        let value = 123;
        return function() {
            alert(value);
        }
    } 
    let g = f(); // 當 g 函式存在時,該值會被保留在記憶體中
    g = null;    // 現在記憶體被清理了

    實際開發中的優化

    正如我們所看到的,理論上當函式可達時,它外部的所有變數也都將存在。但在實際中,JavaScript 引擎會試圖優化它。它們會分析變數的使用情況,如果從程式碼中可以明顯看出有未使用的外部變數,那麼就會將其刪除。

    V8(Chrome,Opera)的一個重要的副作用是,此類變數在除錯中將不可用。

    開啟 Chrome 瀏覽器的開發者工具,並嘗試執行下面的程式碼。

        function f() {
            let value = Math.random();
            function g() {
                debugger;
            }
            return g;
        } 
        let g = f();
        g();

    當代碼執行到“debugger;”這個地方時會暫停,此時在控制檯中輸入 console.log(value);。

    結果:報錯:VM146:1 Uncaught ReferenceError: value is not defined

    這可能會導致有趣的除錯問題。比如:我們可以看到的是一個同名的外部變數,而不是預期的變數:

    let value = "Surprise!";
    function f() {
        let value = "the closest value";
        function g() {
            debugger;
        }
        return g;
    }
    let g = f();
    g();

    當代碼執行到“debugger;”這個地方時會暫停,此時在控制檯中輸入 console.log(value);。

    結果:輸出:Surprise。

    以上就是JavaScript閉包原理及作用詳解的詳細內容,更多關於JavaScript閉包的資料請關注我們其它相關文章!