(注1:如果有問(wèn)題歡迎留言探討矾飞,一起學(xué)習(xí)一膨!轉(zhuǎn)載請(qǐng)注明出處,喜歡可以點(diǎn)個(gè)贊哦H髀佟)
(注2:更多內(nèi)容請(qǐng)查看我的目錄豹绪。)
1. 簡(jiǎn)介
JS入門難點(diǎn)系列到此,我們將進(jìn)行一個(gè)階段性的總結(jié)微谓。將前面所學(xué)的有關(guān)作用域森篷,LHS输钩,RHS,執(zhí)行上下文棧仲智,執(zhí)行上下文买乃,變量對(duì)象,作用域鏈钓辆,this等知識(shí)點(diǎn)串聯(lián)起來(lái)剪验,使大家有一個(gè)更新清晰的認(rèn)識(shí)。
2. 概念回顧
作用域:其實(shí)是一種規(guī)則與概念前联,我們并不能直接訪問(wèn)他功戚,是用來(lái)幫我們區(qū)分變量是在哪里定義的。作用域有三種(ES6之前):全局作用域似嗤,局部(函數(shù))作用域啸臀,eval作用域。ES6之后又多了一個(gè)塊級(jí)作用域烁落。比如乘粒,我們經(jīng)常會(huì)這么說(shuō),在全局作用域定義了一個(gè)變量a伤塌,在當(dāng)前函數(shù)作用域定義了一個(gè)變量a灯萍。雖然是a,但你可以分辨這其實(shí)是兩個(gè)a每聪,因?yàn)槠渌幍淖饔糜虿煌?/p>
作用域鏈:很容易與作用域弄混旦棉。作用域鏈其實(shí)是在進(jìn)入執(zhí)行上下文以后創(chuàng)建的。是由當(dāng)前的活動(dòng)對(duì)象和當(dāng)前函數(shù)的[[scope]]屬性拼接而成药薯。如果在全局執(zhí)行上下文绑洛,則當(dāng)前作用域鏈僅有全局變量對(duì)象。要記住童本,全局變量對(duì)象始終在作用域鏈的頂端诊笤。而在代碼執(zhí)行時(shí),對(duì)變量的查找巾陕,不管是LHS,還是RHS纪他,都是從當(dāng)前作用域開(kāi)始鄙煤,順著作用域鏈向前查找。
LHS茶袒,RHS:引擎的查詢方式有兩種梯刚,即LHS和RHS。代碼中出現(xiàn)變量時(shí)薪寓,如果目的是要進(jìn)行存儲(chǔ)亡资,也就是我們關(guān)心的是要找到變量的容器本身澜共,來(lái)進(jìn)行不同數(shù)據(jù)的存儲(chǔ)賦值操作,而不關(guān)心現(xiàn)在這個(gè)容器里面存的是什么锥腻,就會(huì)用到LHS嗦董。而如果我們的目的只是拿這個(gè)變量來(lái)用,也就是只關(guān)心這個(gè)變量存儲(chǔ)的內(nèi)容是啥瘦黑,而不需要關(guān)心這個(gè)變量存在哪個(gè)容器京革,那么就會(huì)用到RHS。
執(zhí)行上下文:JavaScript 的可執(zhí)行代碼(executable code)有以下三類:全局代碼幸斥、函數(shù)代碼匹摇、eval代碼。當(dāng)JS引擎遇到這三類代碼時(shí)甲葬,會(huì)開(kāi)始做準(zhǔn)備工作廊勃,創(chuàng)建一個(gè)“執(zhí)行上下文(execution context)"。執(zhí)行上下文的生命周期有兩個(gè)階段:準(zhǔn)備階段和代碼執(zhí)行階段经窖。在準(zhǔn)備階段坡垫,會(huì)做三件事,即用arguments創(chuàng)建當(dāng)前執(zhí)行上下文的活動(dòng)對(duì)象钠至,確定當(dāng)前執(zhí)行上下文的作用域鏈葛虐,和綁定當(dāng)前執(zhí)行上下文的this屬性。
執(zhí)行上下文棧:當(dāng)JS執(zhí)行到一個(gè)函數(shù)的時(shí)候棉钧,就會(huì)創(chuàng)建該函數(shù)的“執(zhí)行上下文(execution context)"屿脐。JS代碼中可能出現(xiàn)為數(shù)眾多的函數(shù),所以JavaScript 引擎創(chuàng)建了執(zhí)行上下文棧(Execution context stack宪卿,ECS)來(lái)管理執(zhí)行上下文的诵。
變量對(duì)象:變量對(duì)象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲(chǔ)了在執(zhí)行上下文中定義的變量和函數(shù)聲明佑钾。全局上下文中的變量對(duì)象就是全局對(duì)象西疤。在函數(shù)上下文中,我們用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象休溶〈蓿活動(dòng)對(duì)象是在進(jìn)入函數(shù)上下文時(shí)刻被創(chuàng)建的,它通過(guò)函數(shù)的 arguments 屬性初始化兽掰。arguments 屬性值是 Arguments 對(duì)象芭碍。
this:this 提供了一種更優(yōu)雅的方式來(lái)隱式“傳遞”一個(gè)對(duì)象引用,因此可以將 API 設(shè)計(jì)得更加簡(jiǎn)潔并且易于復(fù)用孽尽。this 是在運(yùn)行時(shí)進(jìn)行綁定的窖壕,并不是在編寫時(shí)綁定,它的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。this 的綁定和函數(shù)聲明的位置沒(méi)有任何關(guān)系瞻讽,只取決于函數(shù)的調(diào)用方式鸳吸。
3.關(guān)鍵點(diǎn)劃分
這么多的概念,在JS編譯執(zhí)行過(guò)程中速勇,很容易讓大家繞暈晌砾。但其實(shí),只要把握住整個(gè)JS編譯執(zhí)行的關(guān)鍵點(diǎn)快集,一切困難都會(huì)迎刃而解贡羔。那么這個(gè)關(guān)鍵點(diǎn)是什么呢?其實(shí)就是函數(shù)執(zhí)行个初。
我們來(lái)看乖寒,只有在執(zhí)行到一個(gè)函數(shù)時(shí),才會(huì)創(chuàng)建函數(shù)執(zhí)行上下文并壓入函數(shù)執(zhí)行上下文棧,不管在當(dāng)前執(zhí)行上下文的準(zhǔn)備階段或者是在執(zhí)行該函數(shù)前做了哪些操作,我們只需要關(guān)心函數(shù)執(zhí)行那一刻的快照迹栓,包括此時(shí)該函數(shù)的[[scope]]屬性值,此時(shí)函數(shù)執(zhí)行上下文棧逐虚,此時(shí)傳入的arguments,此時(shí)函數(shù)的調(diào)用方式谆膳。然后進(jìn)入該函數(shù)上下文叭爱,函數(shù)上下文會(huì)根據(jù)arguments來(lái)初始化活動(dòng)對(duì)象AO,通過(guò)AO+[[scope]]來(lái)確定作用域鏈漱病,并通過(guò)調(diào)用方式確定this綁定买雾。然后進(jìn)入函數(shù)執(zhí)行上下文的代碼執(zhí)行階段,直到遇見(jiàn)下一個(gè)函數(shù)執(zhí)行杨帽,周而復(fù)始或者執(zhí)行完畢還沒(méi)有遇到下一個(gè)函數(shù)執(zhí)行漓穿,執(zhí)行完畢的函數(shù)上下文就會(huì)逐個(gè)出棧并銷毀,直到程序關(guān)閉注盈,全局執(zhí)行環(huán)境銷毀晃危。
4. 流程分析
我們以下面這段代碼為例來(lái)進(jìn)行分析:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope();
現(xiàn)在我們嘗試用之前所學(xué)走一遍代碼的編譯執(zhí)行流程:
- 執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文老客,全局上下文被壓入執(zhí)行上下文棧僚饭。
ECStack = [
globalContext
];
- 全局上下文初始化。(初始化全局環(huán)境的VO胧砰,確定全局環(huán)境的Scope浪慌,綁定全局環(huán)境的this。)當(dāng)然朴则,在此階段,完成了全局作用域的變量聲明和函數(shù)聲明,并且進(jìn)行了全局作用域的變量提升和函數(shù)提升乌妒。
globalContext = {
VO: {
global: window,
scope: undefined,
checkscope: reference to function checkscope
},
Scope: [globalContext.VO],
this: globalContext.VO
}
- checkScope函數(shù)執(zhí)行前階段汹想。初始化的同時(shí),checkscope 函數(shù)被創(chuàng)建撤蚊,保存全局環(huán)境的作用域鏈到函數(shù)checkscope的內(nèi)部屬性[[scope]]古掏。(checkScope在此處是使用函數(shù)聲明,所以猜測(cè)其[[scope]]屬性是在全局環(huán)境初始化階段侦啸,但如果此時(shí)使用變量賦值“var checkscope = function (){...}”這種寫法槽唾,那么此時(shí)checkScope屬性的寫入是在全局環(huán)境的執(zhí)行階段)。但是[[scope]]屬性的創(chuàng)建和寫入時(shí)機(jī)其實(shí)并不是重點(diǎn)光涂,我們說(shuō)了重點(diǎn)是對(duì)關(guān)鍵點(diǎn)的劃分庞萍。就是checkscope函數(shù)執(zhí)行那一刻,[[scope]]屬性是一定已經(jīng)創(chuàng)建并寫入了全局執(zhí)行環(huán)境的作用域鏈即可忘闻。并且執(zhí)行了其他代碼钝计。
checkscope.[[scope]] = [
globalContext.VO
];
globalContext = {
VO: {
global: window,
scope: "global scope",
checkscope: reference to function checkscope
},
Scope: [globalContext.VO],
this: globalContext.VO
}
- 執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文齐佳,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧.
ECStack = [
checkscopeContext,
globalContext
];
- checkscope 函數(shù)執(zhí)行環(huán)境初始化(用 arguments 創(chuàng)建活動(dòng)對(duì)象checkscopeContext.AO私恬,利用活動(dòng)對(duì)象checkscopeContext.AO與checkscope.[[scope]]形成checkscope 函數(shù)執(zhí)行環(huán)境的作用域鏈checkscopeContext.Scope。綁定this到undefined(非嚴(yán)格模式下會(huì)綁定到全局對(duì)象)炼吴。在此階段本鸣,進(jìn)行了加入形參、函數(shù)聲明硅蹦、變量聲明荣德,函數(shù)提升,變量提升等操作提针。
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- checkscope 函數(shù)執(zhí)行環(huán)境初始化的同時(shí)命爬,或者準(zhǔn)確說(shuō),f函數(shù)執(zhí)行之前辐脖,f函數(shù)的[[scope]]屬性創(chuàng)建并寫入饲宛。并且f函數(shù)執(zhí)行前的的其他代碼也會(huì)執(zhí)行。
f.[[scope]] = [
checkscopeContext.AO,
globalContext.VO
];
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: "local scope",
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- 執(zhí)行f函數(shù)嗜价,創(chuàng)建 f 函數(shù)執(zhí)行上下文艇抠,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧。
ECStack = [
fContext,
checkscopeContext,
globalContext
];
- f 函數(shù)執(zhí)行環(huán)境初始化(參考第5步)久锥。
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}
- f函數(shù)中代碼執(zhí)行家淤。需要對(duì)scope進(jìn)行RHS查找。查找從當(dāng)前作用域開(kāi)始沿著作用域鏈向上瑟由。(或者說(shuō)從作用域鏈中的當(dāng)前活動(dòng)對(duì)象開(kāi)始沿著作用域鏈向上查找)絮重。記住,這里是兩種說(shuō)法,但是作用域與活動(dòng)對(duì)象并不等同青伤。
// 查找過(guò)程:
1. fContext.AO.scope 沒(méi)有該變量聲明(即該變量不在當(dāng)前作用域督怜,記住不是為undefined,因?yàn)樵撟兞靠赡艽嬖诤萁牵侵禐閡ndefined)号杠,繼續(xù)
2. checkscopeContext.AO.scope 有該變量聲明,獲取其值為"local scope"
- f 函數(shù)執(zhí)行完畢丰歌,返回"local scope"姨蟋。f 函數(shù)上下文從執(zhí)行上下文棧中彈出。
ECStack = [
checkscopeContext,
globalContext
];
checkscope 函數(shù)在執(zhí)行完f處獲取f執(zhí)行的返回值"local scope"立帖,函數(shù)繼續(xù)向下執(zhí)行眼溶。
checkScope執(zhí)行完畢,返回獲取到的返回值"local scope"厘惦。checkScope 函數(shù)上下文從執(zhí)行上下文棧中彈出偷仿。
ECStack = [
globalContext
];
代碼執(zhí)行流回到全局執(zhí)行環(huán)境中調(diào)用checoscope處,拿到checkScope返回值并繼續(xù)向下執(zhí)行宵蕉。
直到程序終止酝静,或者頁(yè)面關(guān)閉。全局上下文出棧并銷毀羡玛。
參考
JS入門難點(diǎn)解析3-作用域
JS入門難點(diǎn)解析4-執(zhí)行上下文棧
JS入門難點(diǎn)解析5-變量對(duì)象
JS入門難點(diǎn)解析6-作用域鏈
JS入門難點(diǎn)解析7-this
一道js面試題引發(fā)的思考
JavaScript深入之執(zhí)行上下文
深入理解javascript作用域系列第五篇——一張圖理解執(zhí)行環(huán)境和作用域
前端基礎(chǔ)進(jìn)階(二):執(zhí)行上下文詳細(xì)圖解
Javascript變量的作用域和作用域鏈詳解
JavaScript關(guān)于作用域别智、作用域鏈和閉包的理解
理解js中的作用域,作用域鏈以及初探閉包