前言
在學習JavaScript預編譯之前,先了解一下JavaScript從編譯到執行的程序,大致分為四步:
- 詞法分析
- 語法分析:檢查代碼是否存在錯誤,若有錯誤,引擎會拋出語法錯誤,同時會構建一顆抽象語法樹(
AST), - 預編譯
- 解釋執行
預編譯
JavaScript是解釋性語言,也就是說,編譯一行,執行一行,但js并非上來就進入編譯環節,它在編譯之前存在預編譯程序,
js中預編譯一般有兩種:全域的預編譯和函式的預編譯,分別發生在script內代碼執行前和函式的執行前,
函式預編譯
首先來看一個例子:
function test(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
test(1)
就以上述例子中的a為例,有形參a,變數a,函式a,那test函式執行時,此時的a到底是什么呢?
輸出結果:
? a() {}
123
123
? () {}
要想弄明白最終的輸出結果,就不得不好好學習一下預編譯的詳細程序,
在預編譯學習中,經常聽到一句話:函式宣告整體提升,變數宣告提升,
這句話可以解決大多數場景下的預編譯問題,但光憑這句話無法理解透預編譯,接下來我們來一起捋一下函式預編譯的大致流程,
函式預編譯四部曲
- 預編譯開始,會建立
AO(Activation Object)物件 - 找形參和變數宣告,使其作為
AO的屬性名,值賦予undefined - 實參和形參相統一(將實參值賦值給形參)
- 找函式宣告,函式名作為
AO屬性名,值賦予函式體
案例分析
學習了函式的預編譯程序,就可以回頭細細的品味一下上面的案例:
- 先建立
AO,并找形參和變數宣告AO :{ a: undefined, b: undefined } - 形參實參相統一
AO :{ a: 1, b: undefined } - 找函式宣告,值賦予函式體
AO :{ a: function a() {}, b: undefined, d: function d() {} } - 預編譯程序結束,挨著分析一下
console的列印結果:第一個console.log(a); // 此時AO中a的值為function a() {} 執行賦值操作: a = 123 // AO中的a值修改為123 第二個console.log(a) // 123 第三個console.log(a) // 123 b = function() {} // AO中的b值修改為function b(){} console.log(b) // function b(){}
全域預編譯
全域中不存在形參和實參,所以只需處理變數宣告和函式宣告
全域預編譯三部曲
- 生成
GO(Global Object) - 找變數宣告,由于全域變數默認掛載在
window之上,若window當前已存在當前屬性,忽略當前操作,若沒有,變數作為屬性名,值賦予undefined, - 找函式宣告,函式類似與變數,先去
window上查看,不存在,函式作為函式名,值為函式體
案例分析
將函式預編譯案例稍微修改,如下:
// test部分的結果與函式部分相同,再次只分析全域部分
console.log(a);
var a = 1;
console.log(a);
function test(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
console.log(b);
var b = function() {}
console.log(b);
}
test(2);
- 生成
GO,變數提升,函式提升,得到GO如下:GO/window: { a: undefined, test: function() {} } - 因此第一個
a的值為undefined,隨后a賦值為1,所以第二個a的值為1
test中定義了變數a,因此列印的a為自身AO中的值,如果test中沒有定義a,就會沿著作用域鏈,當GO中查找a,
注意事項
1. 當函式中出現同樣名稱的函式名和變數名,編譯器真的會先做變數提升再去函式提升嗎?查看了一些大佬的博客,當出現變數名和函式名相同時,變數提升應該會被忽略掉,不會做重復的無用之功
2. let/const宣告的變數應當同樣進行了變數提升,只不過它與var宣告的變數做了一定的區分
常見面試題分析
題目一
function test() {
console.log(b);
if (a) {
var b = 100;
}
console.log(b);
c = 234;
console.log(c);
}
var a;
test();
a = 10;
console.log(c);
- 生成
GOGO: { a: undefined, test: function test() {}, c: undefined }JavaScript中變數如果未經宣告就賦值,會默認將變數掛載到window物件上,這也就是所謂的
imply global,c就是imply global, - test執行,生成test的AO
// AO還會存盤[[scope]]屬性,存盤AO的作用域鏈 AO: { b: undefined, [[scope]]: [TestAO, GO] }有同學會問,
if(a)為false,if內部不會執行,那test的AO中為什么還會有b啊?預編譯并不是執行,它只不過把變數、函式等進行提升,只有在執行時,才會設計代碼邏輯的判斷, - 分析test函式執行
console.log(b) // AO中b為undefined if (a) // AO中無a,沿[[scope]]找到GO中的a,值為undefined b = 100; // 不執行 console.log(b) // undefined c = 234; // AO中沒有c屬性,沿[[scope]]找到GO中的c修改為234 console.log(c) // 列印的是GO中的c,234 // test執行完畢,AO銷毀 - 分析剩余代碼:
a = 10; // GO中的a修改為10 console.log(c) // GO中c值為234,234
題目二
var foo = 1;
function bar() {
console.log(foo);
if (!foo) {
var foo = 10;
}
console.log(foo);
}
bar();
答案
undefined
10
題目三
var a = 1;
function b() {
console.log(a);
a = 10;
return;
function a() { }
}
b();
console.log(a);
return; 與上面案例的if一樣,預編譯環節不會處理
答案:
function a() { }
1
題目四
console.log(foo);
var foo = "A";
console.log(foo)
var foo = function () {
console.log("B");
}
console.log(foo);
foo();
function foo(){
console.log("C");
}
console.log(foo)
foo();
答案:
? foo(){
console.log("C");
}
A
? () {
console.log("B");
}
B
? () {
console.log("B");
}
B
題目五
var foo = 1;
function bar(a) {
var a1 = a;
var a = foo;
function a() {
console.log(a);
}
a1();
}
bar(3);
答案:
1
總結
預編譯的題目多數情況下就可以采用以下原則:
- 函式宣告,整體提升
- 變數宣告,宣告提升
如果遇到復雜的情況,就要按照最原始的方式一步一步的解決問題,
最后,在預編譯時一定要注意:return、if等代碼邏輯判斷是在執行時候做的,預編譯不管這些,預編譯只管變數、形參、函式等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/306448.html
標籤:其他
