語法解析(檢查有無語法錯誤)
預編譯
解釋運行(將 js 翻譯成計算機識別的語言(0、1組成),翻譯一行執(zhí)行一行)
【全局】:
創(chuàng)建 GO( Grobal Object ) 對象
找變量聲明
找函數(shù)聲明
【函數(shù)】:
創(chuàng)建 AO( Activation Object ) 對象(執(zhí)行上下文);
找形參和變量聲明,將形參和變量名作為 AO 對象的屬性名,值為 undefined(有重復的名稱只寫一個即可);
將形參與實參值統(tǒng)一(用實參的值替換 undefined);
在函數(shù)體中找函數(shù)聲明,將函數(shù)名添加到 AO 對象的屬性中,值為函數(shù)體(如屬性名重復,則覆蓋前面的)。
【執(zhí)行上下文/執(zhí)行環(huán)境】組成:
變量對象(Variable object,VO) :包含變量的對象,無法訪問。
作用域鏈(Scope chain):作用域即變量對象,作用域鏈是一個由變量對象組成的帶頭結(jié)點的單向鏈表,其主要作用就是用來進行變量查找;而[[Scope]]屬性是一個指向這個鏈表頭節(jié)點的指針
this:指向一個環(huán)境對象
【參考】https://www.jianshu.com/p/76ed896bbf91
有時也稱環(huán)境,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù) ,決定了它們各自的行為。而每個執(zhí)行環(huán)境都有一個與之相關(guān)的變量對象,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。
當JavaScript解釋器初始化執(zhí)行代碼時,它首先默認進入全局執(zhí)行環(huán)境,從此刻開始,函數(shù)的每次調(diào)用都會創(chuàng)建一個新的執(zhí)行環(huán)境。
當javascript代碼被瀏覽器載入后,默認最先進入的是一個全局執(zhí)行環(huán)境。
當在全局執(zhí)行環(huán)境中調(diào)用
執(zhí)行一個函數(shù)時,程序流就進入該被調(diào)用函數(shù)內(nèi),此時JS引擎就會為該函數(shù)創(chuàng)建一個新的執(zhí)行環(huán)境,并且將其壓入到執(zhí)行環(huán)境堆棧的頂部。瀏覽器總是執(zhí)行當前在堆棧頂部的執(zhí)行環(huán)境,一旦執(zhí)行完畢,該執(zhí)行環(huán)境就會從堆棧頂部被彈出,然后,進入其下的執(zhí)行環(huán)境執(zhí)行代碼。這樣,堆棧中的執(zhí)行環(huán)境就會被依次執(zhí)行并且彈出堆棧,直到回到全局執(zhí)行環(huán)境。
創(chuàng)建
和執(zhí)行
兩個階段。1、在創(chuàng)建階段,解析器首先會創(chuàng)建一個變量對象
【variable object】(函數(shù)中稱為活動對象
【activation object】),它由定義在執(zhí)行環(huán)境中的變量、函數(shù)聲明、和參數(shù)組成。在這個階段,作用域鏈會被初始化,this的值也會被最終確定。
2、在執(zhí)行階段,代碼被解釋執(zhí)行。
具體過程:每次調(diào)用函數(shù),都會創(chuàng)建新的執(zhí)行上下文。在JavaScript解釋器內(nèi)部,每次調(diào)用執(zhí)行上下文,分為兩個階段:
2.1 創(chuàng)建階段【若是函數(shù),當函數(shù)被調(diào)用,但未執(zhí)行任何其內(nèi)部代碼之前】
在進入執(zhí)行上下文階段,只會將有 var,function
修飾的變量或方法添加到變量對象中。
創(chuàng)建作用域鏈(Scope Chain)
創(chuàng)建變量對象(變量,函數(shù)和參數(shù))
確定this的指向
2.2 激活/代碼執(zhí)行階段:
變量賦值
函數(shù)引用,
解釋/執(zhí)行其他代碼
變量對象
組成函數(shù)的所有形參 (如果是函數(shù)上下文)
由名稱和對應值組成的一個變量對象的屬性被創(chuàng)建
沒有實參,屬性值設(shè)為 undefined
函數(shù)聲明
由名稱和對應值(函數(shù)對象(function-object))組成一個變量對象的屬性被創(chuàng)建
如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性
變量聲明
由名稱和對應值(undefined)組成一個變量對象的屬性被創(chuàng)建;
如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性
可以將每個執(zhí)行上下文抽象為一個對象并有三個屬性:executionContextObj = { scopeChain: { /* 變量對象(variableObject) 所有父執(zhí)行上下文的變量對象*/ }, variableObject: { /*函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ }, this: {} }
1、查找調(diào)用函數(shù)的代碼。2、執(zhí)行函數(shù)代碼之前,先創(chuàng)建執(zhí)行上下文。3、進入創(chuàng)建階段: 3.1 初始化作用域鏈: 3.2 創(chuàng)建變量對象: 3.2.1 創(chuàng)建arguments對象,檢查上下文,初始化參數(shù)名稱和值并創(chuàng)建引用的復制。 3.2.2 掃描上下文的函數(shù)聲明: 為發(fā)現(xiàn)的每一個函數(shù),在變量對象上創(chuàng)建一個屬性——確切的說是函數(shù)的名字——其有一個指向函數(shù)在內(nèi)存中的引用。 如果函數(shù)的名字已經(jīng)存在,引用指針將被重寫。 3.2.3 掃描上下文的變量聲明: 為發(fā)現(xiàn)的每個變量聲明,在變量對象上創(chuàng)建一個屬性——就是變量的名字,并且將變量的值初始化為undefined 如果變量的名字已經(jīng)在變量對象里存在,將不會進行任何操作并繼續(xù)掃描。 3.3 求出上下文內(nèi)部“this”的值。4、激活/代碼執(zhí)行階段:在當前上下文上運行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值。
demo:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { }}foo(22);
1、創(chuàng)建階段:foo(22)函數(shù)調(diào)用時
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... }}
2、執(zhí)行階段:執(zhí)行流進入函數(shù)并且激活/代碼執(zhí)行階段
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, this: { ... }}
注意:
單線程
同步執(zhí)行
唯一的全局執(zhí)行環(huán)境
局部執(zhí)行環(huán)境的個數(shù)沒有限制
每次某個函數(shù)被調(diào)用,就會有個新的局部執(zhí)行環(huán)境為其創(chuàng)建,即使是多次調(diào)用的自身函數(shù)(即一個函數(shù)被調(diào)用多次,也會創(chuàng)建多個不同的局部執(zhí)行環(huán)境)。
Global Code,即全局的、不在任何函數(shù)里面的代碼,例如:一個js文件、嵌入在HTML頁面中的js代碼等。
Function Code,即用戶自定義函數(shù)中的函數(shù)體JS代碼。
Eval Code,即使用eval()函數(shù)動態(tài)執(zhí)行的JS代碼?!静煌扑],可忽略】
或
全局環(huán)境:JavaScript代碼運行起來會首先進入該環(huán)境
函數(shù)環(huán)境:當函數(shù)被調(diào)用執(zhí)行時,會進入當前函數(shù)中執(zhí)行代碼
eval(不建議使用,可忽略)
解析:
全局執(zhí)行環(huán)境
在瀏覽器中,其指window對象,是JS代碼開始運行時的默認環(huán)境。
全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。
函數(shù)執(zhí)行環(huán)境
當某個函數(shù)被調(diào)用時,會先創(chuàng)建一個執(zhí)行環(huán)境及相應的作用域鏈。然后使用arguments和其他命名參數(shù)的值來初始化執(zhí)行環(huán)境的變量對象。
執(zhí)行上下文(execution context)屬性:
變量對象(variableObject)
作用域鏈(scope chain)
this
【注】:
變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。
全局上下文中的變量對象就是全局對象
在函數(shù)上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,只是變量對象是規(guī)范上的或者說是引擎實現(xiàn)上的,不可在 JavaScript 環(huán)境中訪問,只有到當進入一個執(zhí)行上下文中,這個執(zhí)行上下文的變量對象才會被激活,所以才叫 activation object 吶,而只有被激活的變量對象,也就是活動對象
上的各種屬性才能被訪問。
活動對象是在進入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。
****** AO & VO *******
AO = VO function parameters arguments
AO 還包含函數(shù)的 parameters,以及 arguments 這個特殊對象
未進入執(zhí)行階段之前,變量對象(VO)中的屬性都不能訪問!但是進入執(zhí)行階段之后,變量對象(VO)被激活轉(zhuǎn)變?yōu)榱嘶顒訉ο?AO),里面的屬性都能被訪問了,然后開始進行執(zhí)行階段的操作。
它們其實都是同一個對象,只是處于執(zhí)行上下文的不同生命周期
function foo() { console.log(this.a) } function active(fn) { fn(); // 真實調(diào)用者,為獨立調(diào)用 } var a = 20; var obj = { a: 10, getA: foo } active(obj.getA); // 20 var name = "window"; var p = { name: 'Perter', getName: function() { // 利用變量保存的方式保證其訪問的是p對象 var self = this; return function() { console.log(this.name) // window return self.name; } } } var getName = p.getName(); var _name = getName(); console.log(_name); // Perter
var obj = { a: 1, b: function(){ console.log(this); }}
1、作為對象調(diào)用時,指向該對象 obj.b(); // 指向obj
2、作為函數(shù)調(diào)用, var b = obj.b; b(); // 指向全局window
3、作為構(gòu)造函數(shù)調(diào)用 var b = new obj.b(); // this指向當前實例對象
4、作為call與apply調(diào)用 obj.b.apply(object, []); // this指向當前的object
詞法作用域(lexical scoping)是指,函數(shù)在執(zhí)行時,使用的是它被定義時的作用域,而不是這個函數(shù)被調(diào)用時的作用域函數(shù)的作用域在函數(shù)定義的時候就決定了。這是因為函數(shù)有一個內(nèi)部屬性 [[scope]],當函數(shù)創(chuàng)建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]] 就是所有父變量對象的層級鏈,但是注意:[[scope]] 并不代表完整的作用域鏈!當函數(shù)激活時,進入函數(shù)上下文,創(chuàng)建 VO/AO 后,就會將活動對象添加到作用鏈的前端。至此,作用域鏈創(chuàng)建完畢。
demo:
```javascipt
function foo() {
function bar() {
...
}
}
// 函數(shù)創(chuàng)建時,各自的[[scope]]為:
foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.VO ]; ```
``` var scope = "global scope"; function checkscope(){ var scope2 = 'local scope'; return scope2; } checkscope(); ``` 執(zhí)行過程如下: 1.checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到 內(nèi)部屬性[[scope]] ``` checkscope.[[scope]] = [ globalContext.VO ]; ``` 2.執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧 ``` ECStack = [ checkscopeContext, globalContext ]; ``` 3.checkscope 函數(shù)并不立刻執(zhí)行,開始做準備工作,第一步:復制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈 ``` checkscopeContext = { Scope: checkscope.[[scope]], } ``` 4.第二步:用 arguments 創(chuàng)建活動對象,隨后初始化活動對象,加入形參、函數(shù)聲明、變量聲明 ``` checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], } ``` 5.第三步:將活動對象壓入 checkscope 作用域鏈頂端 ``` checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } ``` 6.準備工作做完,開始執(zhí)行函數(shù),隨著函數(shù)的執(zhí)行,修改 AO 的屬性值 ``` checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] } ``` 7.查找到 scope2 的值,返回后函數(shù)執(zhí)行完畢,函數(shù)上下文從執(zhí)行上下文棧中彈出 ``` ECStack = [ globalContext ]; ```
### 閉包* 即使創(chuàng)建它的上下文已經(jīng)銷毀,它仍然存在(比如,內(nèi)部函數(shù)從父函數(shù)中返回)* 在代碼中引用了自由變量內(nèi)部函數(shù)引用了外部函數(shù)的變量,在外部函數(shù)上下文被銷毀后,其中的變量仍然可以被其內(nèi)部函數(shù)引用 因為:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
```
對的,就是因為這個作用域鏈,f 函數(shù)依然可以讀取到 checkscopeContext.AO 的值,說明當 f 函數(shù)引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷毀了,但是 JavaScript 依然會讓 checkscopeContext.AO 活在內(nèi)存中,f 函數(shù)依然可以通過 f 函數(shù)的作用域鏈找到它,正是因為 JavaScript 做到了這一點,從而實現(xiàn)了閉包這個概念。