作用域(scope)規定了變數能夠被訪問的“範圍”,離開了這個“範圍”變數便不能被訪問。作用域分為:局部作用域和全局作用域。 ...
作用域
作用域(scope)規定了變數能夠被訪問的“範圍”,離開了這個“範圍”變數便不能被訪問。
作用域分為:
- 局部作用域
- 全局作用域
局部作用域
局部作用域分為函數作用域和塊作用域。
函數作用域
在函數內部聲明的變數只能在函數內被訪問,外部無法直接訪問。
function foo(){
const bar = 1;
}
console.log(bar); // ReferenceError: bar is not defined
總結:
- 函數內部聲明的變數,在函數外部無法被訪問;
- 函數的參數也是函數內部的局部變數;
- 不同函數內部聲明的變數無法互相訪問;
- 函數執行完畢後,函數內部的變數實際被清空了。
塊作用域
在JavaScript中使用{}
包裹的代碼稱為代碼塊,代碼塊內部聲明的變數外部將有可能無法被訪問。
有可能:取決於使用let
還是var
。
for (let i=1; i<=5; i++){
// i 只能在該代碼塊中被訪問
console.log(i); // 正常
}
// 超出了 i 的作用域
console.log(i); // 報錯
for (var i=1; i<=5; i++){
// i 能在該代碼塊中被訪問
console.log(i); // 正常
}
// 超出了 i 的作用域
console.log(i); // 不會報錯,因為 i 是使用var聲明的
總結:
- let聲明的變數會產生塊作用域,var不會產生塊作用域;
- const聲明的常量也會產生塊作用域;
- 不同代碼塊之間的變數無法互相訪問;
- 推薦使用let或const。
全局作用域
<script>
標簽和.js
文件的最外層就是所謂的全局作用域,在此聲明的變數在函數內部也可以被訪問。
全局作用域中聲明的變數,任何其它作用域都可以被訪問。
註意:
- 為window對象動態添加的屬性預設也是全局的,不推薦!
- 函數中未使用任何關鍵字聲明的變數為全局變數,不推薦!!
- 儘可能少的聲明全局變數,防止全局變數被污染。
作用域鏈
作用域鏈本質上是底層的變數查找機制。
- 在函數被執行時,會優先查找當前函數作用域中查找變數;
- 如果當前作用域查找不到則會依次逐級查找父級作用域直到全局作用域。
總結:
- 嵌套關係的作用域串聯起來形成了作用域鏈
- 相同作用域鏈中按著從小到大的規則查找變數
- 子作用域能夠訪問父作用域,父級作用域無法訪問子級作用域
垃圾回收機制
記憶體的生命周期
JS環境中分配的記憶體,一般有如下生命周期:
- 記憶體分配:當我們聲明變數、函數、對象的時候,系統會自動為他們分配記憶體
- 記憶體使用:即讀寫記憶體,也就是使用變數、函數等
- 記憶體回收:使用完畢,由垃圾回收器自動回收不再使用的記憶體
// 為變數分配記憶體
const num = 10;
// 為對象分配記憶體
const obj = {
num: 10
}
// 為函數分配記憶體
function fn(){
const num = 10;
console.log(num);
}
說明:
- 全局變數一般不會回收(關閉頁面回收)
- 一般情況下局部變數的值,不用了,會被自動回收掉
記憶體泄漏:程式中分配的記憶體由於某種原因程式未釋放或無法釋放叫做記憶體泄漏。
演算法說明
這一部分介紹JS引擎是如何回收記憶體的。
複習
堆棧空間分配區別:
- 棧(操作系統):由操作系統自動分配釋放函數的參數值、局部變數等,基本數據類型放到棧裡面。
- 堆(操作系統):一般由程式員分配釋放,若程式員不釋放,由垃圾回收機制回收。複雜數據類型放到堆裡面。
下麵介紹兩種常見的瀏覽器垃圾回收演算法:引用計數法和標記清除法。
引用計數法
IE採用的引用計數演算法,定義“記憶體不再使用”,就是看一個對象是否有指向它的引用,沒有引用了就回收對象
演算法:
- 跟蹤記錄被引用的次數;
- 如果被引用了一次,那麼就記錄次數1,多次引用會累加;
- 如果減少一個引用就減1;
- 如果引用次數是0,則釋放記憶體。
引用計數演算法是個簡單有效的演算法,但是現在已經很少使用,因為它存在一個致命的問題:嵌套引用(迴圈引用)
如果兩個對象相互引用,儘管他們已不再使用,垃圾回收器不會進行回收,導致記憶體泄漏。
function fn(){
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用計數無法回收'
}
fn()
如上圖,函數執行結束後,局部變數都被取消,但是由於對象相互引用,記憶體無法被回收。
並且,每執行一次函數,就會導致一次記憶體泄漏。
標記清除法
現代的瀏覽器已經不再使用引用計數演算法了。
現代瀏覽器通用的大多是基於標記清除演算法的某些改進演算法,總體思想都是一致的。
核心:
- 標記清除演算法將“不再使用的對象”定義為“無法達到的對象”。
- 就是從根部(在S中就是全局對象)出發定時掃描記憶體中的對象。凡是能從根部到達的對象,都是還需要使用的。
- 那些無法由根部出發觸及到的對象被標記為不再使用,稍後進行回收。
如圖,標記清除法可以解決引用計數法無法解決的相互引用的問題。
閉包
概念:一個函數對周圍狀態的引用捆綁在一起,內層函數中訪問到其外層函數的作用域
簡單理解:閉包 = 內層函數 + 外層函數的變數
function outer(){
const num = 10;
function fn(){
console.log(num);
}
fn();
}
outer(); // 10
內函數fn
使用了外函數的變數num
,形成閉包。
另一個例子:統計函數調用次數,函數調用一次,就加1。
function counter(){
let num = 0;
return function(){
console.log(num++);
}
}
const add = counter();
add(); // 0
add(); // 1
add(); // 2
閉包作用:封閉數據,提供操作,外部也可以訪問函數內部的變數。
閉包應用:實現數據的私有。
變數提升
變數提升是JavaScript中比較“奇怪”的現象,它允許在變數聲明之前即被訪問(僅存在於var聲明變數)
console.log(num); // undefined
var num = 10;
在這個案例中,使用var聲明的num
會存在變數提升的現象。提前輸出不會報錯,是因為這個變數已經聲明瞭。雖然聲明會被提升,但是賦值操作不會被提升,所以num
還是undefined
。
註意:
- 變數在未聲明即被訪問時會報語法錯誤;
- 變數在var聲明之前即被訪問,變數的值為undefined;
- let/const聲明的變數不存在變數提升;
- 變數提升出現在相同作用域當中;
- 實際開發中推薦先聲明再訪問變數。