先來兩段代碼,a 和 o.a 各輸出什么右蹦?
let a = 0;
let b = a;
b++;
alert(a);
let o = {};
o.a = 0;
let b = o;
b.a = 10;
alert(o.a);
應該很多人會回答:a 是 0,o.a 是 10歼捐。
沒錯何陆,但對了一半,因為alert()方法會將輸出結(jié)果執(zhí)行toString()豹储,所以正確答案是:'0' 和 '10'
這里考察的知識點是對js數(shù)據(jù)類型的理解贷盲,也就是能分得清基礎類型和引用類型
js數(shù)據(jù)類型可以分為三類:
- 基本類型(值類型):Number String Boolean Null Undefined
-
引用類型:Object Function
這里可能有人會回答Array,正則等剥扣,但他們其實也是Object巩剖,可以把他們理解為 Object 的分支 -
其他類型:Symbol
ES6新增,創(chuàng)建唯一值
棧內(nèi)存與堆內(nèi)存各自的作用
棧內(nèi)存:提供代碼運行的環(huán)境钠怯,存儲基本類型值
堆內(nèi)存:提供引用類型存儲的環(huán)境空間
回到開始的地方佳魔,將代碼一步步解析,看看在瀏覽器里是怎么執(zhí)行的(深入V8底層實現(xiàn)原理)晦炊。同時使用ProcessOn來繪圖鞠鲜,一步步繪制出執(zhí)行結(jié)果
Step1
瀏覽器加載頁面后,想要代碼自上而下執(zhí)行断国,那么它需要一個執(zhí)行環(huán)境贤姆,而這個執(zhí)行環(huán)境,就是我們所說的全局作用域稳衬,也就是開辟了一個棧內(nèi)存庐氮。
全局作用域?qū)I(yè)名詞為:ECStack(Exeuction Context Stack)
翻譯過來就是執(zhí)行環(huán)境棧,或者叫執(zhí)行上下文棧
Step2
代碼開始執(zhí)行宋彼,解析代碼:let a = 0; let b = a;
所有等號賦值都需要經(jīng)過三個步驟:
創(chuàng)建變量 -> 創(chuàng)建值 -> 關聯(lián)
每個執(zhí)行環(huán)境都有一個變量對象弄砍,也叫值存儲區(qū),Variable Object输涕,簡寫為VO
那么在值存儲區(qū)里就會保存變量a音婶,以及它的值0,然后讓它們之間關聯(lián)起來
接著保存變量b莱坎,b = a衣式,所以b也指向0。有人會理解為b = a,所以是將a的值拷貝一份碴卧,再將b和新的0進行關聯(lián)弱卡,其實并不是,它們都是指向同樣的值0(你可以簡單的理解為這是一個優(yōu)化策略住册,節(jié)省了值的存儲)
更誤:String類型并非存儲在棧內(nèi)存當中婶博,而是存儲在堆內(nèi)存當中,其他基本類型值沒什么問題荧飞,但是對到字符串并非如此凡人,并不是在棧內(nèi)存中如上所述,具體可參考文章:我不知道的JS之JavaScript內(nèi)存模型中的堆空間和椞纠空間
Step3
執(zhí)行代碼b++;
所以此時VO里多存儲了一個值1挠轴,一個變量只能關聯(lián)一個值,所以會先解除b和0的關聯(lián)關系耳幢,并將b跟1相關聯(lián)岸晦。
第二段代碼,也就是引用類型賦值的睛藻,那么它的存儲方式又有所不同
Step1
let o = {}; o.a = 0; let b = o;
在上面棧內(nèi)存與堆內(nèi)存各自的作用里說了委煤,堆內(nèi)存是引用類型存儲的環(huán)境空間。也就是說修档,當執(zhí)行到 {} 的時候碧绞,發(fā)現(xiàn)該值是個引用類型,所以需要將該值存儲到堆內(nèi)存里(前面基本類型值都是存在棧內(nèi)存當中)吱窝,然后將0與堆內(nèi)存的空間地址相關聯(lián)
Step2
執(zhí)行代碼b.a = 10;
此時與b關聯(lián)的存儲空間為AAAFFF000讥邻,那么就會去到該堆內(nèi)存里,將保存值10院峡,并將a與10相關聯(lián)兴使。而o與b都是關聯(lián)的同一個堆內(nèi)存空間地址,所以去獲取o.a的時候照激,值也會變?yōu)?0发魄。
以上,就是為什么基本類型值不會相互產(chǎn)生影響俩垃,而引用類型的值會更改的底層原理励幼。因為基礎類型是與值直接相關聯(lián),而引用類型關聯(lián)的是一個空間地址口柳。
下面各輸出什么苹粟?先別往下翻,自行畫圖并寫出輸出結(jié)果
let a = {
n: 1
};
let b = a;
a.x = a = {
n: 2
};
console.log(a.x);
console.log(b);
這里我就不再畫圖了跃闹,太累嵌削,用文字一步步解釋吧
- 創(chuàng)建變量a毛好,創(chuàng)建值,發(fā)現(xiàn)是個引用類型苛秕,所以新開一個堆內(nèi)存(繼續(xù)假設空間地址為AAAFFF000)肌访,存儲n: 1
- 創(chuàng)建變量b,將b 與 空間地址AAAFFF000相關聯(lián)
- 由于沒有創(chuàng)建變量艇劫,所以來到 創(chuàng)建值 -> 關聯(lián) 這一步吼驶,發(fā)現(xiàn)值是個引用類型,新開一個堆內(nèi)存(假設空間地址為AAAFFF111)港准,存儲n: 2。
- a.x咧欣,此時a關聯(lián)的空間地址為AAAFFF000浅缸,所以在該堆內(nèi)存里創(chuàng)建x,值為{n: 2}
- a關聯(lián)空間地址AAAFFF111魄咕,但注意衩椒,上一步操作已經(jīng)更改了AAAFFF000,所以這一步雖然改變了a的關聯(lián)空間哮兰,但不會對AAAFFF000產(chǎn)生影響毛萌。同時,a.x的關聯(lián)也被解除喝滞,因為a重新關聯(lián)了新的空間地址
總結(jié)以上代碼執(zhí)行后阁将,目前兩個空間地址存儲的值:
AAAFFF000: {n: 1, x: {n: 2}}
AAAFFF111: {n: 1}
因此
第一句:a的關聯(lián)地址是AAAFFF111,里面沒有x右遭,所以輸出undefined
第二句:b的關聯(lián)地址沒變做盅,一直是AAAFFF000,所以輸出{n: 1, x: {n: 2}}
上面的細節(jié)點在于:
一:看到等號窘哈,則要記住等號執(zhí)行的三步操作吹榴,由于a.x = a并沒有創(chuàng)建變量,所以接下來是創(chuàng)建值和關聯(lián)
二: a.x = a = {n: 2}; 等價于a.x = {n: 2}; a = {n: 2}; 注意兩句的順序滚婉,對應上面3图筹、4點
如果文字理解不了,建議按照上面的流程一步步畫圖理解让腹,加深印象
GO/VO/AO/EC及作用域和執(zhí)行上下文
先來幾個名詞
GO:全局對象(Global Object)
ECStack: 執(zhí)行環(huán)節(jié)棧(Exeuction Context Stack)
EC:執(zhí)行環(huán)境(Exeuction Context远剩,也叫執(zhí)行上下文)
|-- VO:變量對象(Variable Object)
|-- AO:活動對象(Activation Object,函數(shù)的叫AO骇窍,理解為VO的一個分支)
Scope:作用域民宿,創(chuàng)建函數(shù)的時候賦予
Scope Chain:作用域鏈
這里多了一個詞,EC像鸡,在上面只說了ECStack活鹰,并沒有說EC哈恰,因為放在函數(shù)這塊說更合適,也就是之前那篇文章里說的執(zhí)行上下文三種類型:
- 全局執(zhí)行上下文
- 函數(shù)執(zhí)行上下文
- Eval 函數(shù)執(zhí)行上下文
先來一段代碼:
let x = 1;
function A(y) {
let x = 2;
function B(z) {
console.log(x+y+z);
}
return B;
}
let C = A(2);
C(3);
用圖文的方式一步步說明代碼是如何執(zhí)行的
瀏覽器開啟志群,創(chuàng)建ECStack着绷,用于執(zhí)行代碼
-
創(chuàng)建EC(G),全局上下文
創(chuàng)建EC -
創(chuàng)建完成锌云,進棧荠医,也就是EC進入ECStack,這個過程叫:進棧執(zhí)行
進棧執(zhí)行 -
執(zhí)行
let x = 1; function A() {...};
桑涎,這一步我就不再畫堆內(nèi)存了彬向,畫起來還是很浪費時間的莺禁。發(fā)現(xiàn)有函數(shù)谷饿,需要添加函數(shù)[[scope]]屬性。所有在當前的上下文當中呀狼,只要創(chuàng)建了函數(shù)等曼,那么必然會給函數(shù)添加[[scope]]屬性里烦。所以說,實際上執(zhí)行上下文跟作用域本質(zhì)是兩個不同的東西禁谦。
發(fā)現(xiàn)函數(shù)胁黑,添加函數(shù)作用域[[scope]]屬性 -
執(zhí)行
c = A(2);
,要執(zhí)行函數(shù)A州泊,那么需要創(chuàng)建新的上下文丧蘸,把EC(G)壓至棧底,然后進棧執(zhí)行遥皂。
A函數(shù)進棧執(zhí)行 -
執(zhí)行函數(shù)A触趴,并把2賦值給形參y。
執(zhí)行函數(shù)前渴肉,需要做一些準備工作冗懦,先記錄自己的作用域[scope] -> AO(A),還有作用域鏈scopeChain仇祭,scopeChain保存著函數(shù)的鏈式關系(也就是上一層作用域是誰披蕉,再上一層又是誰),當某個變量在該作用域中查找不到的時候乌奇,就會去上層作用域查找没讲。準備工作完成就能執(zhí)行函數(shù)了,創(chuàng)建屬性arguments(因為arguments是類數(shù)組礁苗,所以我這里就用[0: 2]來表示)及其他函數(shù)中的變量爬凑。作用域是在函數(shù)創(chuàng)建的時候就有的,而作用域鏈是在函數(shù)執(zhí)行的時候才產(chǎn)生的
執(zhí)行函數(shù)
- 最后一句執(zhí)行c的我就不再畫了试伙,原理同上嘁信,創(chuàng)建EC(B)巴拉巴拉巴拉…
最后附上偽代碼
// 第一步:創(chuàng)建全局執(zhí)行上下文于样,并將其壓入ECStack中
ECStack = [
// 全局執(zhí)行上下文
EC(G): {
..., // 包含全局對象原有屬性
x = 1;
A = function(y){...};
A[[scope]] = VO(G); // 創(chuàng)建函數(shù)的時候就確定了其作用域
}
]
// 第二步:執(zhí)行函數(shù)A(2)
ECStack = [
// A的執(zhí)行上下文
EC(A): {
// 鏈表初始化為:AO(A) -> VO(G)
[scope]: VO(G),
scopeChain: <AO(A), VO(G)>
// 創(chuàng)建函數(shù)A的活動對象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function(z){...},
B[[scope]] = AO(A),
this: window
}
},
// 全局執(zhí)行上下文
EC(G): {
..., // 包含全局對象原有屬性
x = 1;
A = function(y){...};
A[[scope]] = VO(G); // 創(chuàng)建函數(shù)的時候就確定了其作用域
}
]
// 第三步:執(zhí)行B/C函數(shù) C(3)
ECStack = [
// B的執(zhí)行上下文
EC(B): {
[scope]: AO(A),
scopeChain: <AO(B), AO(A), VO(G)>
// 創(chuàng)建函數(shù)A的活動對象
AO(B): {
arguments: [0: 3],
z: 3,
this: window
}
},
// A的執(zhí)行上下文
EC(A): {
// 鏈表初始化為:AO(A) -> VO(G)
[scope]: VO(G),
scopeChain: <AO(A), VO(G)>
// 創(chuàng)建函數(shù)A的活動對象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function(z){...},
B[[scope]] = AO(A),
this: window
}
},
// 全局執(zhí)行上下文
EC(G): {
..., // 包含全局對象原有屬性
x = 1;
A = function(y){...};
A[[scope]] = VO(G); // 創(chuàng)建函數(shù)的時候就確定了其作用域
}
]
檢驗學習情況的時候到了,下面這道題請動手畫圖潘靖,并輸出正確結(jié)果:
let a = 12,
b = 12;
function fn () {
let a = b = 13;
console.log(a, b);
}
fn();
console.log(a, b);
圖我就不畫了穿剖,自行練手吧,這里有一個額外的知識點需要說一下卦溢,let a = b = 13
這個轉(zhuǎn)化后糊余,應該是let a = 13; b = 13
,所以在EC(fn)上下文中单寂,是沒有變量b的贬芥,那么它會去上層作用域鏈中查找并更改。
因此輸出答案:13 13; 12 13
額外拓展練習:閉包
來一道題宣决,這里其實使用上面的知識已經(jīng)能答出正確答案了才對蘸劈,當你畫出圖后,你也就能看出為什么說閉包會導致沒法釋放內(nèi)存了(形成無法銷毀的上下文)
let i = 1;
let fn = (i) => (n) => console.log(n + (++i));
let f = fn(1); // 形成閉包
f(2);
fn(3)(4); // 并沒有形成閉包疲扎,兩個上下文都可以被釋放
f(5);
console.log(i);
// 上面箭頭函數(shù)的代表以下代碼塊
// let fn = function (i) {
// return function (n) {
// return console.log((n + (++i));
// }
// }
由于EC(fn)中返回的匿名函數(shù)被變量 f 所引用昵时,所以可以理解為f=function () {console.log(n + (++i))}
捷雕,EC(F)執(zhí)行后上下文會被銷毀椒丧,但由于變量f引用了EC(fn)中的匿名函數(shù),導致EC(fn)不能被銷毀救巷,所以變量對象AO(fn)就會一直存在壶熏,因此i
一直都能被EC(f)所訪問,還被EC(f)一直修改浦译,這就形成了閉包棒假。
fn(3)(4);
雖然也是閉包,但它可以釋放精盅,因為EC(fn)內(nèi)部有沒被外部所引用的帽哑。(圖沒畫是因為再畫整個圖就亂得沒法看了)
閉包的作用有兩個:保存和保護,對到這個例子i
一直沒法被釋放就是保存叹俏,i
沒法被外部所訪問到就是保護
這道題需要手動做標記妻枕,標記i在每次執(zhí)行后值為多少
不懂最好自行一步步畫圖,因為函數(shù)有形參i粘驰,因此相當于EC(fn)中有自己的變量i屡谐,并且被保存著(函數(shù)柯里化),還有++i和i++的區(qū)別蝌数,++1是在執(zhí)行的時候已經(jīng)疊加愕掏,i++是執(zhí)行完才會有加1,分得清這兩個知識點后顶伞,自行標記一下應該就能得出正確答案了: 4; 8; 8; 1;