1. 變數物件和堆記憶體
變數物件是生成執行上下文時建立的一個特殊的物件。JS的基礎資料型別(number,string,boolean,undefined),按值訪問,通常儲存在變數物件中。JS的引用資料型別(如物件、陣列、函式, new Number(4))的值儲存在堆記憶體中,而JS不允許直接訪問堆記憶體,只能通過引用訪問。引用其實就是儲存在變數物件中的一個地址指標,這個地址指向堆記憶體中的實際值。
?:
var a = 0 // 變數a和值0都寸與變數中物件
var b = 'string' // 變數b和值0都寸與變數中物件
var c = null // 變數c和值0都寸與變數中物件
var d = { m: 1 } // 變數d存在於變數物件中,{m: 1} 作為物件存在於堆記憶體中
var e = [1, 2, 3] // 變數e存在於變數物件中,[1, 2, 3] 作為物件存在於堆記憶體中
var f = function () {...} // 變數f存在於變數物件中,function作為物件存在於堆記憶體中
複製程式碼
記憶體儲存方式示意圖:
訪問引用資料型別時,實際上是先從變數物件中獲取地址,再根據地址從堆記憶體中取值。
?:
// demo1
var a = 20;
var b = a;
b = 30;
console.log(a) // 20
複製程式碼
// demo2
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a) // 15
複製程式碼
2. 執行上下文
執行上下文可以理解為程式碼的執行環境,會形成一個作用域。JS主要有兩種執行上下文:
- 全域性執行上下文:JS程式碼執行起來會首先進入全域性執行上下文
- 函式執行上下文:當函式被呼叫執行時,會進入當前函式的執行上下文
JS程式碼通常是函式內呼叫函式,必然產生多個執行上下文,JS引擎會以棧(FILO)的方式來處理他們,這個棧就是函式呼叫棧(callstack)。函式呼叫棧棧底定是全域性執行上下文,棧頂是當前正在執行的函式的執行上下文。棧頂的上下文執行完之後,該上下文出棧。
?:
function fn1 () {
var a = 1
function fn2 () {
const b = 2
console.log(a + b)
}
fn2()
}
fn1()
複製程式碼
Call Stack示意圖:
瀏覽器檢視程式碼執行過程的call stack:
3. 變數物件(Variable Object)
本節探討的時生成執行上下文的時候都做了什麼。 執行上下文生命週期:
執行上下文的建立階段其實就是為程式碼執行做準備,準備工作就包含了生成變數物件。 變數物件的生成過程包含以下三個步驟。
- 建立arguments物件。
- 檢查函式宣告(function):在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用。如果函式名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
- 檢查變數宣告(var):每找到一個變數宣告,就在變數物件中以變數名建立一個屬性,屬性值為undefined。如果該變數名的屬性已經存在,為了防止同名的函式被修改為undefined,則會直接跳過,原屬性值不會被修改。
- 注意:函式宣告比變數宣告優先順序高。
建立完成之後在程式碼執行階段,JS解析器就能在變數物件中找到宣告的變數或者函式,進行一系列的操作。現在明白變數提升咋個回事兒了吧。
?:
function fn1 () {
console.log(a) // undefined
console.log(fn2) // f fn2(){}
console.log(fn2()) // 2
var a = 1
function fn2 () {
return 2
}
console.log(a) // 1
}
fn1()
複製程式碼
建立階段:fn1執行上下文,建立階段生成變數物件
fn1EC = {
VO: {
arguments: { ... },
fn1: <fn1 reference>,
a: undefined
}
...
}
複製程式碼
執行階段: VO --> AO(Active Object)
fn1EC = {
AO: {
arguments: { ... },
fn1: <fn1 reference>,
a: 1
}
...
}
複製程式碼
執行階段變數物件變為活動物件,可以訪問屬性了,上面程式碼相當於
function fn1 () {
function fn2 () {
return 2
}
var a
console.log(a) // undefined
console.log(fn2) // f fn2(){}
console.log(fn2()) // 2
a = 1
console.log(a) // 1
}
fn1()
複製程式碼
- 注:全域性上下文VO = window
4. 作用域鏈
第二節講到在執行上下文的建立階段,有三個任務:建立變數物件、建立作用域鏈、明確this指向。 本節講作用域鏈。
作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了當前執行環境對符合訪問許可權的變數和函式的有序訪問.
?:
var a = 1
function fn1 () {
var b = a + 1
var c = 3
function fn2 () {
var c = 4
return b + c // 6
}
fn2()
}
fn1()
複製程式碼
我們知道,fn1能夠訪問全域性變數a,fn2能夠訪問fn1中的變數b、c,但是c使用的是本作用域的c,反過來fn1不能訪問fn2種的變數c,JS引擎是如何實現變數的查詢?答案就是:沿著作用域鏈查詢
fn2 Scope Chain:
5. 閉包
JS的函式外部無法讀取到函式內的區域性變數:
function fn1() {
var a = 1
console.log(a)
}
console.log(a) // error
複製程式碼
如何從外部讀取區域性變數:
var a = 1
function fn1 () {
var b = a + 1
function fn2 () {
console.log(b) // 2
console.log(c) // error: c is not defined
}
return fn2
}
var res = fn1()
function fn3() {
var c = 3
res()
}
fn3()
複製程式碼
在函式fn1內部定義一個函式fn2,fn2內訪問fn1的變數,並把fn2作為返回值,在外部執行fn2的時候,就訪問到fn1的區域性變數了。 這就形成了閉包,有些地方稱fn1是閉包,有的稱fn2為閉包, 我們和chrom保持一致,稱父函式fn1為閉包。
閉包可以理解為集中技巧,使得在函式外部能夠訪問函式內部的變數,且這些變數的值始終儲存在記憶體中。例子中,fn1是fn2的父函式,而fn2被賦給了一個全域性變數,這導致fn2始終在記憶體中,而fn2的存在依賴於fn1,因此fn1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制回收。
此時:fn2 ScopeChain = [fn2 VO, fn1 VO, global VO], 因此fn2能訪問b, 但不能訪問c, fn1執行完了,fn1已經出棧,但是fn1並沒有被釋放。
所以,使用閉包會造成記憶體佔用較大。
練習:
for (var i=0; i<5; i++) {
setTimeout( function () {
console.log(i);
}, i*1000 );
}
複製程式碼
輸出什麼??
如何改動實現輸出1,2,3,4,5: 每次迴圈將i值儲存到了閉包中:
for (var i=0; i<5; i++) {
setTimeout((function (i) {
function () {
console.log(i);
}, i*1000 )
}(i)
}
複製程式碼
閉包最大的應用就是模組化。
菜鳥一隻,若有不對,歡迎並感謝大家指正?。