33 個(gè) js 核心概念(一): 函數(shù)調(diào)用棧斗搞,執(zhí)行上下文及變量對象

前言

為什么會有這篇文章?

在書籍或博客上慷妙,我們經(jīng)常會看到「作用域鏈」僻焚、「閉包」、「變量提升」等概念膝擂,說明一個(gè)問題 —— 它們很重要虑啤。

但很多時(shí)候,對于這些概念架馋,看的時(shí)候覺得自己已經(jīng)明白了狞山,可過不了多久,再讓你說一說叉寂,可能就說不清楚了萍启,之所以會這樣,是因?yàn)槲覀儗τ?JavaScript 這門語言的運(yùn)行機(jī)制不清楚屏鳍。

我相信搞明白了今天所講的內(nèi)容勘纯,會對你理解那些知識大有裨益!

函數(shù)調(diào)用棧(call stack)

1. 什么是棧钓瞭?

類似 js 中的數(shù)組驳遵,棧也是用來存儲數(shù)據(jù)的一種數(shù)據(jù)結(jié)構(gòu)。他的特點(diǎn)是后進(jìn)先出(LIFO)山涡。

與之相對的一種數(shù)據(jù)結(jié)構(gòu)稱為隊(duì)列超埋,隊(duì)列的特點(diǎn)是先進(jìn)先出(FIFO)搏讶。

可以想象這樣一種場景:小明和同學(xué)們放學(xué)回家佳鳖,老師讓他在排在隊(duì)伍的最前面霍殴,他們每天回家路上都要經(jīng)過一個(gè)胡同,小明每次都是第一個(gè)進(jìn)入胡同系吩,肯定也是第一個(gè)出來来庭,這就是所謂「先進(jìn)先出」。

可是有一天穿挨,小明他們走到胡同里發(fā)現(xiàn)胡同口停了一輛車月弛,把胡同給堵死了,沒辦法科盛,他們只能隊(duì)頭變隊(duì)尾往回撤帽衙,這時(shí)候,小明雖然最先進(jìn)入胡同贞绵,卻只能最后出去厉萝,最先出去的是排在隊(duì)尾的小華,也就是「后進(jìn)先出」榨崩。

2. 什么叫函數(shù)調(diào)用棧?

在 js 中函數(shù)的調(diào)用也遵照這樣以一個(gè)原則:最先調(diào)用的函數(shù)先放到調(diào)用棧中谴垫,假如這個(gè)函數(shù)內(nèi)部又調(diào)用了別的函數(shù),那么這個(gè)內(nèi)部函數(shù)就接著被放入調(diào)用棧中母蛛,直至不再有函數(shù)調(diào)用翩剪。最先執(zhí)行完畢的一定是最里面的函數(shù),執(zhí)行過后彈出調(diào)用棧彩郊,接著執(zhí)行上一層函數(shù)前弯,直至所有函數(shù)執(zhí)行完,調(diào)用棧清空秫逝。

這樣說可能會不太明白恕出,舉個(gè)例子:

// 其他語句
function first() {
 console.log('first')
 function second() {
     console.log("second")
 }
 second();
 third();
 // 其他語句
}
//其他語句
function third() {
    console.log("third")
}
// 調(diào)用 first
first();

在上述代碼中,首先調(diào)用的是函數(shù) first, 此時(shí) first 進(jìn)入函數(shù)棧筷登,接著在 first 中調(diào)用函數(shù) second,second 入棧剃根,當(dāng) second 執(zhí)行完畢后,second 出棧前方,third 入棧狈醉,接著 third 執(zhí)行完出棧,執(zhí)行 first 其他代碼惠险,直至 first 執(zhí)行完苗傅,函數(shù)棧清空。

函數(shù)調(diào)用棧

執(zhí)行上下文(Execution Context)

1. 什么是執(zhí)行上下文班巩?

js 代碼在執(zhí)行時(shí)渣慕,會進(jìn)入一個(gè)執(zhí)行環(huán)境嘶炭,它會形成一個(gè)作用域。這個(gè)執(zhí)行環(huán)境逊桦,便是執(zhí)行上下文眨猎。

JavaScript 主要有三種執(zhí)行環(huán)境:

  1. 全局執(zhí)行環(huán)境: 代碼開始執(zhí)行時(shí)首先進(jìn)入的環(huán)境。
  2. 函數(shù)環(huán)境:函數(shù)調(diào)用時(shí)强经,會開始執(zhí)行函數(shù)中的代碼睡陪。
  3. eval:不建議使用,可忽略匿情。

2. 執(zhí)行上下文的生命周期

上面講到 js 代碼執(zhí)行時(shí)會生成一個(gè)執(zhí)行上下文兰迫。而這個(gè)執(zhí)行上下文的周期,分為兩個(gè)階段:

  1. 創(chuàng)建階段炬称。這個(gè)階段會生成變量對象(VO)汁果,建立作用域鏈以及確定 this 的值。
  2. 執(zhí)行階段玲躯。這個(gè)階段進(jìn)行變量賦值据德,函數(shù)引用及執(zhí)行代碼。
    到這里你應(yīng)該就會明白府蔗,上面函數(shù)調(diào)用棧晋控,就是生成了一個(gè)函數(shù)的執(zhí)行上下文。
    執(zhí)行上下文的生命周期

3. 什么是執(zhí)行棧姓赤?

執(zhí)行上下文也同樣遵循函數(shù)調(diào)用棧的規(guī)則赡译,無非就是多加了一層 —— 全局執(zhí)行上下文,函數(shù)執(zhí)行完后會跳出執(zhí)行棧不铆,而全局執(zhí)行上下文蝌焚,會在關(guān)閉瀏覽器后跳出執(zhí)行棧。

還是上面的例子誓斥,我們看一下執(zhí)行棧只洒。

執(zhí)行上下文

變量對象

1. 什么叫變量對象?

從上面其實(shí)可以得到答案劳坑,變量對象是 js 代碼在進(jìn)入執(zhí)行上下文時(shí)毕谴,js 引擎在內(nèi)存中建立的一個(gè)對象,用來存放當(dāng)前執(zhí)行環(huán)境中的變量距芬。

2. 變量對象(VO)的創(chuàng)建過程

變量對象的創(chuàng)建涝开,是在執(zhí)行上下文創(chuàng)建階段,依次經(jīng)過以下三個(gè)過程:

  1. 創(chuàng)建 arguments 對象框仔。對于函數(shù)執(zhí)行環(huán)境舀武,首先查詢是否有傳入的實(shí)參,如果有离斩,則會將參數(shù)名是實(shí)參值組成的鍵值對放入arguments 對象中银舱,否則瘪匿,將參數(shù)名和 undefined,組成的鍵值對放入 arguments 對象中寻馏。
      function bar(a, b, c) {
        console.log(arguments);  // [2, 4]
        console.log(arguments[2]); // undefined
      }
      bar(2,4)
    
  2. 檢查當(dāng)前環(huán)境中的函數(shù)聲明棋弥。當(dāng)遇到同名的函數(shù)時(shí),后面的會覆蓋前面的操软。
    console.log(a); // function a() {console.log('fjdsfs') }
    function a() {
        console.log('24');
    }
    function a() {
      console.log('fjdsfs')
    }
    
    在上面的例子中嘁锯,在執(zhí)行第一行代碼之前,函數(shù)聲明已經(jīng)創(chuàng)建完成聂薪,后面的對之前的聲明進(jìn)行了覆蓋。
  3. 檢查當(dāng)前環(huán)境中的變量聲明并賦值為undefined蝗羊。當(dāng)遇到同名的函數(shù)聲明藏澳,為了避免函數(shù)被賦值為 undefined ,會忽略此聲明
    console.log(a); // function a() {console.log('fjdsfs') }
    console.log(b); // undefined
    function a() {
      console.log('24');
    }
    function a() {
    console.log('fjdsfs');
    }
    var b = 'bbbbbbbb';
    var a = 46;
    
    在上例我們可以看到,在代碼之前前耀找,a 仍舊是一個(gè)函數(shù)翔悠,而 b 是 undefined。

根據(jù)以上三個(gè)步驟野芒,對于變量提升也就知道是怎么回事了蓄愁。

3. 變量對象變?yōu)榛顒?dòng)對象

執(zhí)行上下文的第二個(gè)階段,稱為執(zhí)行階段狞悲,在此時(shí)撮抓,會進(jìn)行變量賦值,函數(shù)引用并執(zhí)行其他代碼摇锋,此時(shí)丹拯,變量對象變?yōu)?strong>活動(dòng)對象。

我們還是舉上面的例子:

   console.log(a); // function a() {console.log('fjdsfs') }
   console.log(b); // undefined
   function a() {
       console.log('24');
   }
   function a() {
     console.log('fjdsfs');
   }
   var b = 'bbbb';
   console.log(b); // 'bbbb'
   var a = 46; 
   console.log(a);  // 46
   var  b = 'hahahah';
   console.log(b); // 'hahah'

在上面的代碼中荸恕,代碼真正開始執(zhí)行是從第一行 console.log() 開始的乖酬,自這之前,執(zhí)行上下文是這樣的:

// 創(chuàng)建過程
EC= {
  VO: {}; // 創(chuàng)建變量對象
  scopeChain: {}; // 作用域鏈
}
VO = {
  argument: {...}; // 當(dāng)前為全局上下文融求,所以這個(gè)屬性值是空的
  a: <a reference> // 函數(shù) a  的引用地址
  b: undefiend  // 見上文創(chuàng)建變量對象的第三步
}

根據(jù)步驟咬像,首先是 arguments 對象的創(chuàng)建;其次生宛,是檢查函數(shù)的聲明县昂,此時(shí),函數(shù) a 聲明了兩次茅糜,后一次將覆蓋前一次七芭;最后,是檢查變量的聲明蔑赘,先聲明了變量 b狸驳,將它賦值為 undefined预明,接著遇到 a 的聲明,由于 a 已經(jīng)聲明為了一個(gè)函數(shù)耙箍,所以撰糠,此條聲明將會被忽略。

到此辩昆,變量對象的創(chuàng)建階段完成阅酪,接下來時(shí)執(zhí)行階段,我們一步一步來汁针。

  1. 執(zhí)行 console.log(a),我們知道术辐,此時(shí) a 是第二個(gè)函數(shù),所以會輸出function a() {...}施无;

  2. 執(zhí)行 console.log(b)辉词,不出我們所料,將會輸出 undefined猾骡;

  3. 執(zhí)行賦值操作: b = 'bbbb'瑞躺;

  4. 執(zhí)行 console.log(b) ,此時(shí)兴想,b 已經(jīng)賦值幢哨,所以會輸出 'bbbb'

  5. 執(zhí)行賦值操作: a = 46;

  6. 執(zhí)行 console.log(a) 嫂便,此時(shí)捞镰,a 的值變?yōu)?46。

  7. 執(zhí)行賦值操作: b = 'hahahah'顽悼;

  8. 執(zhí)行 console.log(b)曼振, b 已經(jīng)被重新賦值,輸出 hahahah蔚龙。

由上面我們可以看到冰评,在執(zhí)行階段,變量對象是跟著代碼不斷變化的木羹,此時(shí)甲雅,我們把變量對象成為活動(dòng)對象。

執(zhí)行到最后一步時(shí)坑填,執(zhí)行上下文變成了這樣抛人。

// 執(zhí)行階段
EC = {
  VO = {};
  scopeChain: {};
}
 // VO ---- AO
AO = {
  argument: {...};
  a: 46;
  b: 'hahahah';
  this: window;
}

以上,就是變量對象在代碼執(zhí)行前及執(zhí)行時(shí)的變化脐瑰。

剛開始就說過妖枚,這部分概念將會對你理解后面的知識有很大的幫助,所以剛開始接觸的話可能會有些晦澀苍在,建議就是認(rèn)真讀兩遍绝页,結(jié)合后面的知識荠商,經(jīng)常回過頭來看看续誉。

最后留一道題莱没,給大家作為練手,觀察觀察執(zhí)行上下文及變量對象的變化酷鸦。

console.log(a);
console.log(b);
var a = 4;
function a() {
  console.log('我是a1');
  b(3, 5);
}
var a = function a() {
  console.log('我是a2');
  b(3, 5);
}
var b = function (m, n) {
   console.log(arguments);
   console.log('b')
}
a();

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饰躲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子臼隔,更是在濱河造成了極大的恐慌嘹裂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躬翁,死亡現(xiàn)場離奇詭異焦蘑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盒发,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狡逢,“玉大人宁舰,你說我怎么就攤上這事∩莼耄” “怎么了蛮艰?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長雀彼。 經(jīng)常有香客問我壤蚜,道長,這世上最難降的妖魔是什么徊哑? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任袜刷,我火速辦了婚禮,結(jié)果婚禮上莺丑,老公的妹妹穿的比我還像新娘著蟹。我一直安慰自己,他們只是感情好梢莽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布萧豆。 她就那樣靜靜地躺著,像睡著了一般昏名。 火紅的嫁衣襯著肌膚如雪涮雷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天轻局,我揣著相機(jī)與錄音洪鸭,去河邊找鬼样刷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卿嘲,可吹牛的內(nèi)容都是我干的颂斜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拾枣,長吁一口氣:“原來是場噩夢啊……” “哼沃疮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梅肤,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤司蔬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后姨蝴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俊啼,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年左医,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了授帕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浮梢,死狀恐怖跛十,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秕硝,我是刑警寧澤芥映,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站远豺,受9級特大地震影響奈偏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜躯护,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一惊来、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榛做,春花似錦唁盏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锰瘸,卻和暖如春刽严,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工舞萄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留眨补,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓倒脓,卻偏偏與公主長得像撑螺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子崎弃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容