這個前端學習筆記是學習gitchat上的一個課程,這個課程的質量非常好,價格也不貴,非常時候前端入門的小夥伴們進階。
筆記不會涉及很多,主要是提取一些知識點,詳細的大家最好去過一遍教程,相信你一定會有很大的收穫
作用域
就近原則,通過作用域鏈,找到最近的一個變量
-
ES6的塊級作用域
使用
let
和const
命名的變量,不會聲明提升。在上述2個聲明的變量前使用變量會報錯,並且同一個作用域下只允許一個
let
聲明同一個變量。 -
函數的參數也會出現死區TDZ,所以需要注意引用的順序。
執行上下文和調用棧
- 執行上下文就是當前代碼的執行環境/作用域
代碼執行會經歷2個階段:
-
代碼預編譯(VO)
降JS代碼編譯成可執行的代碼。javascript是解釋型語言,編譯一行執行一行。執行前,javascript引擎會提前做些準備工作。確認語法無誤的時候,javascript代碼在預編譯的階段對變量的內存空間進行分配,在這過程中會經歷:
- 變量聲明
- 變量聲明提升,值爲undefined
- 非表達式函數聲明提升
-
代碼執行階段(AO)
此時作用域鏈已經確定,由當前作用域和外層所決定,保證了變量的有序訪問。
調用棧
函數一個接着一個調用,形成的類似棧訪問。符合先進後出的形式。
函數執行完之後,退出棧之後,函數的內部變量就會被垃圾回收器回收,這也是函數外部無法訪問函數內部的原因。
正常的函數會經歷3個階段:
- 創建階段
- 執行階段
- 執行完畢,回收階段
閉包
閉包就是爲了解決無法訪問函數內部的這個問題,函數執行完畢,空間不會馬上銷燬。
內層函數引用了外層函數作用域下的變量,並且內層函數在全局環境下可訪問,就形成了閉包。
一個簡單的閉包:
function numGenerator() {
let num = 1
num++
return () => {
console.log(num)
}
}
var getNum = numGenerator()// 函數執行之後,num不會馬上消失
getNum()// 可以通過返回的函數進行訪問
我們如果返回一個函數,而這個函數訪問了上一個函數作用域下的變量,這樣就能夠讓函數上下文在函數執行之後不會馬上銷燬。這也是閉包的基本原理。
內存管理
- 分配內存:聲明的時候,會劃分內存保存數據
- 使用內存:所有使用到變量的地方
- 銷燬內存:手動等於null,或者執行完畢之後走出上下文環境等
堆內存和棧內存
javascript有2種數據類型:基本和引用
基本類型都保存在棧內存中,而引用類型保存在堆內存中,注意引用內存也會使用到棧內存,引用類型棧內存保存的是堆內存的地址。這裏用一張圖表示:
每一個語言都會有屬於自己的垃圾回收器,在不用到的內存會進行釋放,但是垃圾回收器也是不完美的,也會存在判斷錯誤的情況,無法將垃圾內存進行回收。這樣就會出現內存泄漏:指內存空間明明已經不再被使用,但由於某種原因並沒有被釋放的現象。
內存泄漏
這個是每一個程序員都需要關心的問題。我們來看看前端有那些常見的內存泄漏情形。
-
例子1:
var element = document.querySelectorAll("li")
// 移除 element 節點
function remove() {
document.querySelector(‘body’).removeChild(element1[0])
}
```
我們在操作DOM元素的時候,如果響應移除掉一個元素節點,一般使用removeChild
即可,但是上述情況中,element
還保存着對已經移除的元素引用,雖然視覺上這個節點已經移除了,但是在dom
對象上,還是會保存這個節點的信息。所以,我們需要手動的移除element
這個節點。
-
例子2
var element = document.getElementById('element') element.innerHTML = '<button id="button">點擊</button>' var button = document.getElementById('button') button.addEventListener('click', function() { // ... }) element.innerHTML = ''
這裏我們動態添加一個節點,但是我們給這個按鈕添加了一個事件之後,想要清楚
element
節點的內容,雖然視覺上已經移除,但是在內存中,button
還保存着button
節點的信息和其事件處理句柄還在,垃圾回收器無法回收,需要手動清楚這些引用。 -
例子3
定時器,如果不需要了就及時停止。
function foo() { var name = 'lucas' window.setInterval(function() { console.log(name) }, 1000) } foo()
由於計時器的一直存在,
name
無法釋放內存。如果業務不需要,就自己手動停止計時器。
clearInterval
-
意外的全局變量
function foo(arg) { bar = "this is a hidden global variable"; } // 或者不恰當使用this function foo() { this.variable = "potential accidental global"; }
函數執行後,並不會消除
bar
的內存
垃圾回收機制
當然,除了開發者主動保證以外,大部分的場景瀏覽器都會依靠:
-
引用計數
這是一個算法,能夠追蹤沒有被引用的對象,進行回收。通過例子來理解這個引用的意思。
var obj1 = { property1: { subproperty1: 20 } };
obj1
引用了一個對象property1
,property1
也引用了一個對象。由於obj1
引用了一個對象,所以不會被回收。我們接下來繼續進行操作:
var obj2 = obj1; obj1 = "some random text"
這時,
obj2
引用這obj1
所引用的同一個對象,後面obj1
不在引用對象,這時只存在一個obj2
引用着這個對象。垃圾回收器也不會回收。我們繼續操作:
var obj2_pro = obj2.property1
obj2_pro
引用這obj2
屬性。這個時候,對象就有2個引用,一個是obj2
,一個是obj2_pro
我們消除
obj2
的引用obj2 = "some random text"
但是還存在
obj2_pro
的引用,垃圾回收器無法回收。我們繼續操作,消除obj2_pro
的引用。obj2_pro = null
這個時候,最初的對象沒有任何引用了,這時引用垃圾回收器就可以進行回收了。
假如,2個對象互相引用,那麼這個就永遠無法回收了。這就會導致內存泄漏。
-
標記清除
爲了解決引用計數的弊端,這裏還存在一種回收機制。
我們從跟節點進行訪問,把所有訪問到的進行標記,所有可以遍歷都遍歷之後,存在沒有被標記的對象,就可以進行回收。
自 2012 年以來,JavaScript 引擎已經使用此算法來代替引用計數垃圾回收。