全面攻克js中的堆棧內(nèi)存及閉包

先來兩段代碼,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ù)類型可以分為三類:

  1. 基本類型(值類型):Number String Boolean Null Undefined
  2. 引用類型:Object Function
    這里可能有人會回答Array,正則等剥扣,但他們其實也是Object巩剖,可以把他們理解為 Object 的分支
  3. 其他類型: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í)行上下文棧

ECStack

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é)省了值的存儲)

創(chuàng)建及保存變量

更誤: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)岸晦。

b重新關聯(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)

引用類型 - 存儲于堆內(nèi)存

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发魄。

引用類型 - 存儲于堆內(nèi)存

以上,就是為什么基本類型值不會相互產(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);

這里我就不再畫圖了跃闹,太累嵌削,用文字一步步解釋吧

  1. 創(chuàng)建變量a毛好,創(chuàng)建值,發(fā)現(xiàn)是個引用類型苛秕,所以新開一個堆內(nèi)存(繼續(xù)假設空間地址為AAAFFF000)肌访,存儲n: 1
  2. 創(chuàng)建變量b,將b 與 空間地址AAAFFF000相關聯(lián)
  3. 由于沒有創(chuàng)建變量艇劫,所以來到 創(chuàng)建值 -> 關聯(lián) 這一步吼驶,發(fā)現(xiàn)值是個引用類型,新開一個堆內(nèi)存(假設空間地址為AAAFFF111)港准,存儲n: 2。
  4. a.x咧欣,此時a關聯(lián)的空間地址為AAAFFF000浅缸,所以在該堆內(nèi)存里創(chuàng)建x,值為{n: 2}
  5. 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í)行的

  1. 瀏覽器開啟志群,創(chuàng)建ECStack着绷,用于執(zhí)行代碼

  2. 創(chuàng)建EC(G),全局上下文


    創(chuàng)建EC
  3. 創(chuàng)建完成锌云,進棧荠医,也就是EC進入ECStack,這個過程叫:進棧執(zhí)行


    進棧執(zhí)行
  4. 執(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]]屬性

  5. 執(zhí)行c = A(2);,要執(zhí)行函數(shù)A州泊,那么需要創(chuàng)建新的上下文丧蘸,把EC(G)壓至棧底,然后進棧執(zhí)行遥皂。

    A函數(shù)進棧執(zhí)行

  6. 執(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ù)

  1. 最后一句執(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;

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饵撑,一起剝皮案震驚了整個濱河市剑梳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肄梨,老刑警劉巖阻荒,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異众羡,居然都是意外死亡侨赡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門粱侣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來羊壹,“玉大人,你說我怎么就攤上這事齐婴∮兔ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵柠偶,是天一觀的道長情妖。 經(jīng)常有香客問我,道長诱担,這世上最難降的妖魔是什么毡证? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮蔫仙,結(jié)果婚禮上料睛,老公的妹妹穿的比我還像新娘。我一直安慰自己摇邦,他們只是感情好恤煞,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著施籍,像睡著了一般居扒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丑慎,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天喜喂,我揣著相機與錄音,去河邊找鬼立哑。 笑死夜惭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的铛绰。 我是一名探鬼主播诈茧,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捂掰!你這毒婦竟也來了敢会?” 一聲冷哼從身側(cè)響起曾沈,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸥昏,沒想到半個月后塞俱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡吏垮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年障涯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膳汪。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡唯蝶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遗嗽,到底是詐尸還是另有隱情粘我,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布痹换,位于F島的核電站征字,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏娇豫。R本人自食惡果不足惜匙姜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锤躁。 院中可真熱鬧搁料,春花似錦或详、人聲如沸系羞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椒振。三九已至,卻和暖如春梧乘,著一層夾襖步出監(jiān)牢的瞬間澎迎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工选调, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留夹供,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓仁堪,卻偏偏與公主長得像哮洽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弦聂,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350