侯策《前端開發核心知識進階》讀書筆記——Javascript中的Closure


塊級作用域和暫時性死區

變量提升現象:

function foo() {
    console.log(bar)
    var bar = 3
}
foo()
//undefined
function foo() {
    console.log(bar)
    let bar = 3
}
foo()
//Uncaught ReferenceError: bar is not defined

暫時性死區(TDZ——Temporal Dead Zone):

函數默認值受TDZ的影響

function foo(arg1 = arg2, arg2) {
    console.log(`${arg1} ${arg2}`)
}

foo(undefined, 'arg2')

// Uncaught ReferenceError: arg2 is not defined

執行上下文和調用棧

JavaScript 執行主要分為兩個階段:

  • 代碼預編譯階段
  • 代碼執行階段

預編譯階段是前置階段,這個時候由編譯器將 JavaScript 代碼編譯成可執行的代碼。 

執行階段主要任務是執行代碼,執行上下文在這個階段全部創建完成。

預編譯過程做的事情:

  • 預編譯階段進行變量聲明;
  • 預編譯階段變量聲明進行提升,但是值為 undefined;
  • 預編譯階段所有非表達式的函數聲明進行提升。
function bar() {
    console.log('bar1')
}

var bar = function () {
    console.log('bar2')
}

bar()
//bar2

var bar = function () {
    console.log('bar2')
}

function bar() {
    console.log('bar1')
}

bar()
//bar2

思考題:

foo(10)
function foo (num) {
    console.log(foo)
    foo = num;       
    console.log(foo)
    var foo
} 
console.log(foo)
foo = 1
console.log(foo)

輸出:

undefined
10
ƒ foo (num) {
    console.log(foo)
    foo = num     
    console.log(foo)
    var foo
}
1

作用域在預編譯階段確定,但是作用域鏈是在執行上下文的創建階段完全生成的。因為函數在調用時,才會開始創建對應的執行上下文。執行上下文包括了:變量對象、作用域鏈以及 this 的指向。

 

我們在執行一個函數時,如果這個函數又調用了另外一個函數,而這個“另外一個函數”也調用了“另外一個函數”,便形成了一系列的調用棧。

調用關系:foo1 → foo2 → foo3 → foo4。這個過程是 foo1 先入棧,緊接着 foo1 調用 foo2foo2入棧,以此類推,foo3foo4,直到 foo4 執行完 —— foo4 先出棧,foo3 再出棧,接着是 foo2 出棧,最后是 foo1 出棧。這個過程“先進后出”(“后進先出”),因此稱為調用棧。

注意:正常來講,在函數執行完畢並出棧時,函數內局部變量在下一個垃圾回收節點會被回收,該函數對應的執行上下文將會被銷毀,這也正是我們在外界無法訪問函數內定義的變量的原因。也就是說,只有在函數執行時,相關函數可以訪問該變量,該變量在預編譯階段進行創建,在執行階段進行激活,在函數執行完畢后,相關上下文被銷毀。

閉包

函數嵌套函數時,內層函數引用了外層函數作用域下的變量,並且內層函數在全局環境下可訪問,就形成了閉包。

function numGenerator() {
    let num = 1
    num++
    return () => {
        console.log(num)
    } 
}

var getNum = numGenerator()
getNum()

內存管理

內存的生命周期:

  • 分配內存空間
  • 讀寫內存
  • 釋放內存空間
var foo = 'bar' // 在堆內存中給變量分配空間
alert(foo)  // 使用內存
foo = null // 釋放內存空間

js中基本數據類型和引用數據類型的存儲方式可以參看之前的博客

基本數據類型和引用數據類型的區別

內存泄漏場景

只是把 id 為 element 的節點移除,但是變量 element 依然存在,該節點占有的內存無法被釋放。

var element = document.getElementById("element")
element.mark = "marked"

// 移除 element 節點
function remove() {
    element.parentNode.removeChild(element)
}

需要在 remove 方法中添加:element = null,這樣更為穩妥。

var element = document.getElementById('element')
element.innerHTML = '<button id="button">點擊</button>'

var button = document.getElementById('button')
button.addEventListener('click', function() {
    // ...
})

element.innerHTML = ''

 element.innerHTML = '',button 元素已經從 DOM 中移除了,但是由於其事件處理句柄還在,所以依然無法被垃圾回收。我們還需要增加 removeEventListener,防止內存泄漏。

瀏覽器垃圾回收

大部分的場景瀏覽器都會依靠以下兩種算法來進行垃圾回收:

  • 標記清除
  • 引用計數

具體實現方式可以參看之前的文章:JS垃圾回收機制 

閉包帶來的內存泄漏

借助閉包來綁定數據變量,可以保護這些數據變量的內存塊在閉包存活時,始終不被垃圾回收機制回收。因此,閉包使用不當,極可能引發內存泄漏,需要格外注意。

 

function foo() {
    let value = 123

    function bar() { alert(value) }

    return bar
}

let bar = foo()

 

可以看出,變量 value 將會保存在內存中,如果加上:

bar = null

隨着 bar 不再被引用,value 也會被清除。

 

閉包實現單例模式 

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

意圖:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

主要解決:一個全局使用的類頻繁地創建與銷毀。

何時使用:當您想控制實例數目,節省系統資源的時候。

如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建。

關鍵代碼:構造函數是私有的。

function Person() {
    this.name = 'lucas'
}

const getSingleInstance = (function(){
    var singleInstance
    return function() {
         if (singleInstance) {
            return singleInstance
         } 
        return singleInstance = new Person()
    }
})()

const instance1 = getSingleInstance()
const instance2 = getSingleInstance()

我們有 instance1 === instance2。因為借助閉包變量 singleInstance,instance1 和 instance2 是同一引用的(singleInstance),這正是單例模式的體現。

 

 

參考資料:

侯策 前端開發核心知識進階

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM