JS解析器執行原理和聲明提升機制
文章目錄
一.概念
瀏覽器中有一套專門解析JS代碼的程序,這個程序稱爲JS的解析器
瀏覽器運行整個頁面文檔時,遇到< script > 標籤時JS解析器開始解析JS代碼
二.JS解析器的工作步驟
1.預解析代碼
主要找一些關鍵字如 var,function,以及函數的參數等,並存儲進倉庫裏面,也就是內存
先掃描全局的代碼,在函數執行的時候,然後掃描局部的,也就是函數內部的
-
變量的初始值是undefind
//聲明一個a變量,放進倉庫scope console.log(a);//undefined var a = 1;
-
函數的初始值就是該函數的代碼塊(而不是undefined)
console.log(test);//打印函數代碼塊 //聲明一個函數,放進倉庫scope function test() {...} //控制檯打印函數代碼塊 ƒ test() { return 1; }
-
當變量和函數重名時,不管順序誰前誰後,只留下函數的值
console.log(test);//打印函數代碼塊 function test() { return 1; } var test=2; //控制檯打印函數代碼塊 ƒ test() { return 1; }
注意:這裏是預解析的時候,函數是一等公民,比變量優先級高
-
當函數和函數重名的時候,只會留下後面的那個函數,會遵從上下文機制
console.log(test); function test() { return 1; } //重名時,預解析只會留下後面這個函數 function test() { return 234; } var test=2; //控制檯打印函數代碼塊 ƒ test() { return 234; }
2.逐行執行代碼
當預解析完成之後,就開始執行代碼,倉庫中變量的值,隨時都可能發生變化
1. alert(a);// function a(){alert(3);}
2. var a = 1;
3. alert(a);//1
4. function a() { alert(2); }
5. alert(a);//1
6. var a = 3;
7. alert(a);//3
8. function a() { alert(3); }
9. alert(a);//3
/*
解讀代碼
預解析過程:
第2行:找到了一個var 關鍵字,聲明一個變量a
第4行:找到了一個function關鍵字 把a變成了一個函數
第6行:找到了一個var關鍵字,但是由於聲明變量名字與函數重名,所以不起作用,a的值還是一個函數,因爲
函數比變量的優先級高
第8行:找到了一個function關鍵字,a還是一個函數,但是把第4行的給覆蓋了,因爲函數重名,會遵從上下
文機制
最後預解析得到的結果是:
a => fn -> function a(){alert(3);}
原因: 1.當變量和函數重名時,不管順序誰前誰後,只留下函數的值
2.當函數和函數重名的時候,只會留下後面的那個函數,會遵從上下文機制
逐行代碼執行過程:
第1行: 彈出最後的名字叫a的函數代碼塊
alert(a);// function a(){alert(3);}
第3行: 因爲第2行將a從函數變成了變量,並賦值爲1
alert(a);//1
第5行: 因爲第4行只是聲明瞭一個函數,不會自己執行,所以a的值保持不變
alert(a);//1
第7行:因爲第6行將a的值變成了3
alert(a);//3
第9行:因爲第8行只是聲明一函數,不會自己執行,所以a的值保持不變
*/
3. 示例解讀
像這種示例,如果一眼無法看出來的話,使用解析器原理去看待題目,就沒有解析不出來的,如果熟了,基本上就一眼就可以看出來,因爲一步一步解析太麻煩了.
3.1 示例1(全局變量和函數內部變量名相同)
1.var a = 1;
2.function test(x) {
3. alert(x);
4. alert(a);
5. var a = 2;
6. alert(a);
7. }
8. test();
/*
解讀代碼
預解析過程:掃描代碼,尋找var,function,以及函數的參數,放進倉庫
先掃描全局的var function
global scope=>{
a => undefined
text =>function test(){}
}
逐行代碼執行過程:
第1行,a賦值爲1
第2行,只是聲明函數,不會執行
第8行,調用函數,開始進行局部掃描,也就是函數內部進行預解析
test scope=>{
x => undefined
a => undefined
}
test()逐行執行函數內部的代碼:
第3行:alert(x)=> undefined
第4行:alert(a)=> undefined
第5行,a賦值爲2
第6行:alert(a)=> 2
*/
3.2 示例2 (變量和函數重名)
當變量和函數重名時,不管順序誰前誰後,只留下函數的值
1.alert(typeof fn);//function
2.var fn = 10;
3.function fn() { };
4.alert(typeof fn);//number
/*
解析代碼
預解析:
global scope=>{
fn=> function fn() { };
}
執行代碼:
1.alert(typeof fn); 會彈出function
2.賦值fn=10,此時fn類型變成了number
3.不會執行
4.alert(typeof fn); //number
*/
3.3 示例3(函數內部使用全局變量)
1.var a = 1;
2.function fn() {
3. alert(a);//1
4. a = 2;
5.}
6.fn();
7.alert(a);//2
/*
解析代碼
預解析:
global scope=>{
a=>undefined
fn=> function fn() { };
fn scope=>{
a=>undefined
}
}
執行代碼:
1.a賦值等於1
2.不會執行
6.執行函數,函數沒有參數,也沒有var關鍵字進行聲明,不會預解析
3.第3行:alert(a) 訪問的是全局變量 a 彈出1
4.第4行:將全局變量a 賦值爲了2
7.第7行:彈出2
*/
3.4 示例4(局部變量不會改變全局變量)
1.var a = 1;
2.function fn(a) {
3. alert(a);//undefined
4. a = 2;
5. alert(a);//2
6.}
7.fn();
8.alert(a);//1
/*
解析代碼
預解析:
global scope=>{
a=> undefined
fn=>function fn(a) {...}
fn scope=>{
a=>undefined//預解析形參
}
}
執行代碼:
先調用fn函數 fn函數內部彈出a爲undefined
函數內部 a賦值爲2
函數內部 執行第二個alert(a) 爲2
函數執行完之後,繼續執行下一步 alert(a) 爲1因爲函數函數內部雖然給了a=2
但是至少改的函數內部的值,不會改全局變量 的a
*/
3.5 示例5
console.log(num);// undefined
var num = 24;
console.log(num);// 24
func(100, 200);
function func(num1, num2) {
var total = num1 + num2;
console.log(total);// 300
}
很自然的一段代碼,不要受前面的影響,而不知道最最簡單的代碼了
3.6 示例6.(同名函數)
fn();//2
function fn() { console.log(1); }
fn();//2
var fn = 10;
fn();//報錯 fn is not a function
function fn() { console.log(2); }
fn();
/*
解析代碼
預解析:
global scope=>{
fn =>function fn() { console.log(2); }
}
同名函數時,後面覆蓋前面
變量和函數同名時,函數是一等公民,優先級高
逐行執行代碼時:
第4行:將fn變成了一個number類型,再執行fn()時,函數不存在了,會直接報錯,停止執行
*/
三.聲明提升機制
在 JavaScrip 中變量聲明和函數聲明,聲明會被提升到當前作用域的頂部。
講聲明提升機制之前,要先說說JavaScript中的作用域,這裏排除ES6
1.作用域
在Javascript中,作用域爲可訪問變量(包含對象和函數)的集合
也就是說:作用域就是起作用的範圍
1.全局作用域
整個頁面起作用,在script內部都能訪問到
全局作用域中有全局對象window,代表一個瀏覽器窗口,可以直接調用
全局作用域中聲明的變量和函數,會作爲window對象的屬性和方法保存
變量在所有函數外聲明,也就是全局變量,擁有全局作用域
<script>
var a = 123;//全局變量
function fn() {
console.log(a);//123
}
fn();
console.log(a);//123
</script>
在JavaScript中,函數是唯一擁有自身作用域的代碼塊
2.局部作用域
局部作用域內的變量只能在函數內部使用,所以也叫函數作用域
變量在函數內聲明,即爲局部變量,擁有局部作用域
<script>
function fn() {
var a = 123;//全局變量
console.log(a);//123
}
fn();
console.log(a);//報錯:Uncaught ReferenceError: a is not defined
</script>
注意:
- 可以直接給一個未聲明的變量賦值(全局變量),但不能直接使用未聲明的變量!
- 由於局部變量只作用於函數內部,所以不同的函數內部可以相同名稱的變量
- 當全局與局部有同名變量的時候,訪問該變量將遵循"就近原則"
2.變量的生命週期
全局變量在頁面打開時創建,在頁面關閉後銷燬
局部變量在函數開始執行時創建,函數執行完之後局部變量自動銷燬
3.聲明提升機制
JavaScript 的變量聲明具有聲明提升機制,JavaScript引擎在執行的時候,會把所有變量的聲明都提升到當前作用域的頂部
我們可以從JavaScript解析器的執行原理來理解聲明提升機制,就很好理解了,同樣,上述解析器執行的示例代碼中,都是聲明提升機制的一種體現.當然了,這裏是不考慮ES6的.
總結一下:
-
javascript是沒有塊級作用域的,函數是javascript中唯一擁有自身作用域的結構
-
聲明變量,實際上就是定義了一個名字,在內存中開闢了存儲空間,並且初始爲undefined,提升到當前作用域頂部
-
函數的參數是原始類型的值(數值,字符串,布爾值),傳值方式是傳值傳遞,也就是在函數體內修改參數值,不會影響到函數外部
var a = 1; function fn(a) { console.log(a);//3 } fn(3); console.log(a);//1 不會改變原始值
-
函數的參數是複合類型,也就是引用類型(數組,對象,函數)的時候,傳值方式是傳址傳遞,傳入的是原始值的地址,所以在函數內部修改參數,會改變原始的值(可參考深淺拷貝原理)
var obj = { a: 1, b: 2 }; function fn(obj) { obj.a=2; } fn(obj); console.log(obj);//{a: 2, b: 2}
注意:如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值!
比如數組:
var arr = [1, 2, 3] function fn(array) array = [1, 2]; } fn(arr); console.log(arr);//[1, 2, 3]
4.聲明提升機制示例
- var a 變量聲明,提升到作用域頂部,
- var a=1; 變量聲明,提升到作用域頂部,但是賦值部分不會被提升
- var a=function(){ … } 函數表達式,但也只是變量的聲明提升,並不會提升函數值
- function a() {…} 函數聲明,會全部提升,而且如果函數名字和變量名字相同的話.優先級比變量高,因爲是一等公民
總之呢,理解聲明提升機制,好好去理解JS解析器的原理就好了.