js運行步驟預(yù)編譯【全局】: 【函數(shù)】: 創(chuàng)建 AO( Activation Object ) 對象(執(zhí)行上下文); 找形參和變量聲明,將形參和變量名作為 AO 對象的屬性名,值為 undefined(有重復(fù)的名稱只寫一個即可); 將形參與實參值統(tǒng)一(用實參的值替換 undefined); 在函數(shù)體中找函數(shù)聲明,將函數(shù)名添加到 AO 對象的屬性中,值為函數(shù)體(如屬性名重復(fù),則覆蓋前面的)。
【執(zhí)行上下文/執(zhí)行環(huán)境】組成: 【參考】https://www.jianshu.com/p/76ed896bbf91 執(zhí)行環(huán)境(執(zhí)行上下文 execution context)定義有時也稱環(huán)境,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù) ,決定了它們各自的行為。而每個執(zhí)行環(huán)境都有一個與之相關(guān)的變量對象,環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。 執(zhí)行過程當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)境。 執(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ù)上下文) 由名稱和對應(yīng)值組成的一個變量對象的屬性被創(chuàng)建 沒有實參,屬性值設(shè)為 undefined 函數(shù)聲明 由名稱和對應(yīng)值(函數(shù)對象(function-object))組成一個變量對象的屬性被創(chuàng)建 如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性 變量聲明 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建; 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性
可以將每個執(zhí)行上下文抽象為一個對象并有三個屬性:
executionContextObj = {
scopeChain: { /* 變量對象(variableObject) 所有父執(zhí)行上下文的變量對象*/ },
variableObject: { /*函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ },
this: {}
}
解釋器執(zhí)行代碼的偽邏輯(函數(shù))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)建引用的復(fù)制。
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)境)。
執(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)境及相應(yīng)的作用域鏈。然后使用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í)行上下文的不同生命周期 函數(shù)參數(shù)的傳遞賦值問題 與 this的指向問題函數(shù)參數(shù)的傳遞賦值問題 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
this指向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 作用域鏈創(chuàng)建詞法作用域(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
];
```
函數(shù)執(zhí)行上下文中作用域鏈和變量對象的創(chuàng)建過程總結(jié) ```
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í)行,開始做準備工作,第一步:復(fù)制函數(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)了閉包這個概念。
|