塊級作用域和暫時性死區
變量提升現象:
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 調用 foo2,foo2入棧,以此類推,foo3、foo4,直到 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),這正是單例模式的體現。
參考資料:
侯策 前端開發核心知識進階
