背景
JS基礎知識溫習。
函式解析原理
分3個階段:
- 準備(Hoisting)
- 裝載(填充資料)
- 執行(逐行處理)
準備
本階段就是書本中所說的Hoisting,包括:形參變數建立
、函式體內變數提升建立
和 函式申明建立
。
就是先把函式中所有的變數或者宣告的函式名都先定義好,空間都開闢好。
關於準備階段的特別說明:
- 如果變數已經定義過,則不會重新定義(比如:形參中有個引數a,並且呼叫函式時傳了值進來,這時候函式中還有個變數a,那麼在這一階段,函式中的變數a是不會重新定義,形參中的值也不會被覆蓋。);
裝載
這裡裝載填充的資料包括:形參
和 申明的函式體
。
也許你要問了,為什麼一般的變數只是拿到前面定義好,此時值是 undefined
,填充資料需要等到執行那一行才進行,而 形參
和 申明的函式
在程式碼執行前就要裝載好呢?
答: 我個人的理解是(有更專業的解釋,歡迎批評指正):
形參
是外部傳進來的,是函式在執行前就已經知道的資料,所以直接就裝載上;而對於函式中普通的變數,受限於JS解析順序的機制影響,只能等到具體執行到那一行時才能知道。函式申明
為什麼要放到前面去呢?這應該也是JS的策略吧,不然函式表示式(var xxx=fn)為啥就沒這個待遇呢?
關於裝載的特別說明:
- 資料裝載的資料為:
函式形參
>函式申明
; - 對於函式宣告裝載時,如果已經有相同的函式名的話,會覆蓋前面的(比如:形參中有個引數a,並且外部給他賦了值,這時候函式中如果有個函式申明a,那麼在這一階段,形參中的a就會被覆蓋為這個函式)。
執行
通過上面的2個階段,大家就知道,當函式真正一行行開始執行的時候,其實有些值已經存在了,並不是大家想象中的全部為 undefined
。
本階段就是純粹的執行程式碼了,執行就包括了:變數賦值、物件呼叫等等。
但是本階段其實JS引擎還做了另外一件事情,就是:程式碼檢查
。如果報錯了,會直接中斷程式,除非使用 try/catch
捕獲。
示例一
function test() {
console.log('1、a=',a);
var a = b = 123;
console.log('2、a=',a);
console.log('3、b=',b);
};
test();
console.log('4、b=',b);
console.log('5、a=',a);
複製程式碼
分析
第一步:準備
變數提升(Hoisting),這一步執行後,實際的程式碼變為:
function test() {
var a;
console.log('1、a=',a);
b=123;
a=b;
console.log('2、a=',a);
console.log('3、b=',b);
};
test();
console.log('4、b=',b);
console.log('5、a=',a);
複製程式碼
補充說明: 關於 var a = b = 123
的解釋,請移步: JS解惑-連等表示式
第二步:裝載
由於函式沒有形參和函式申明,所以該步直接跳過。
第三步:執行
//1 var a;
//2 console.log('1、a=',a);
//3 b=123;
//4 a=b;
//5 console.log('2、a=',a);
//6 console.log('3、b=',b);
複製程式碼
- //1 變數定義:a,預設值:
undefined
; - //2 列印變數:a,輸出:
1、a= undefined
; - //3 給變數b賦值為123,由於變數b在test函式中未定義,所以js引擎就會預設在全域性物件
window
所對應的物件下面建立屬性b,並且為其賦值為123,window.b=123; - //4 將b的值賦給a,這時候a就等於123,a=123;
- //5 列印變數:a,輸出:
2、a= 123
; - //6 列印變數:b,輸出:
3、b= 123
;
最後:函式執行完後,區域性變數立即銷燬,全域性變數仍然保留。
//7 console.log('4、b=',b);
//8 console.log('5、a=',a);
複製程式碼
- //7 由於b為全域性變數,執行完函式後未被銷燬,所以輸出:
4、b= 123
; - //8 由於a在函式執行後已經銷燬,而全域性變數又沒有a,所以列印時就報錯了;
最終結果
補充說明: 錯誤型別為 ReferenceError
引用錯誤,也就是說系統根本不知道哪個物件或者函式下面的屬性a,所以會報這個錯誤。如果這時候你列印 window.a
,那麼結果將是 undefined
而不會報錯。
示例二
function test(a) {
console.log('1、a=',a);
var a=123;
console.log('2、a=',a);
function a(){};
console.log('3、a=',a);
};
test(1);
複製程式碼
分析
第一步:準備
定義變數a,程式碼變為:
function test(a) {
var a;
console.log('1、a=',a);
a=123;
console.log('2、a=',a);
function a(){};
console.log('3、a=',a);
};
複製程式碼
第二步:裝載
- 先將形參的值1賦值給a,a=1;
- 將函式申明賦值給a,a=function(){};
裝載完畢後,程式碼變為:
function test(a) {
var a = function(){};
console.log('1、a=',a);
a=123;
console.log('2、a=',a);
console.log('3、a=',a);
};
複製程式碼
第三步:執行
看到第二步裝載完畢後的程式碼,那麼結果也就很清楚了。
最終結果
示例三
function test(a,b){
console.log('1、a=',a);
c=0;
var c;
console.log('2、c=',c);
a=3;
b=2;
console.log('3、b=',b);
function b(){};
function d(){};
console.log('4、b=',b);
};
test(1);
複製程式碼
分析
準備
找到所有的區域性變數,注意包含形參中的,包括:
- a,來自:形參
- b,來自:形參、函式申明
- c,來自:區域性變數
- d,來自:函式申明
於是原函式就變為:
function test(a,b){
var a;
var b;
var c;
var d;
console.log('1、a=',a);
c=0;
console.log('2、c=',c);
a=3;
b=2;
console.log('3、b=',b);
function b(){};
function d(){};
console.log('4、b=',b);
};
test(1);
複製程式碼
裝載
注意裝載的順序:形參先裝載,其次是函式宣告,而且函式申明會覆蓋已定義的變數。
於是函式就變成為:
function test(){
var a=1;
var b=function(){};//實參並沒有傳第2個引數,預設為undefined;但後來又被函式申明覆蓋了。
var c=undefined;//沒有地方為其賦值
var d=function(){};
console.log('1、a=',a);
c=0;
console.log('2、c=',c);
a=3;
b=2;
console.log('3、b=',b);
console.log('4、b=',b);
};
test(1);
複製程式碼
執行
先看看裝載後的函式的執行結果:
所以,其實這類題目最難的就是分析階段,包括:(準備、裝載),一旦這2個階段處理好,執行階段基本就是直接列印結果了。
最終結果
示例四
最後一道帶一些邏輯,可能會影響到分析階段的。
function test(a,b){
console.log('2、b=',b);
if(a){
var b=100;
};
console.log('3、b=',b);
c=456;
console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼
分析
準備
這類題目,先不用管函式體外的程式碼,因為函式準備和裝載,跟外部程式碼怎麼執行沒關係。
- a,來自形參、區域性變數;
- b,來自形參、區域性變數;(注意:函式的準備階段,是不用管是否有if的,只要看到var b,就一定會提前)
對於變數前沒有var申明的,說明是全域性變數,不用理會。
於是程式碼變為:
function test(a,b){
var a;
var b;
console.log('2、b=',b);
if(a){
b=100;
};
console.log('3、b=',b);
c=456;
console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼
裝載
裝載階段依然只管函式體內。
function test(){
var a=undefined;
var b=undefined;
console.log('2、b=',b);
if(a){
b=100;
};
console.log('3、b=',b);
c=456;
console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼
執行
先增加一個行號:
function test(){
var a=undefined;
var b=undefined;
//7 console.log('2、b=',b);
if(a){
b=100;
};
//8 console.log('3、b=',b);
c=456;
//9 console.log('4、c=',c);
};
//1 var a;
//2 console.log('1、a=',a);
//3 test();
//4 a=10;
//5 console.log('5、a=',a);
//6 console.log('6、c=',c);
複製程式碼
- 第1行、第2行,由於沒有test參與,所以結果直接就列印:a=undefined;
- 第3行開始進入函式體內;
- 第7行,b=undefined;
- 第8行,因為上方if(a)條件為假,所以b並沒有賦值100,所以:b=undefined;
- 第9行,由於區域性並沒有變數c,於是就找全域性變數c,這時候恰恰是上方賦值的c=456,所以這時候:c=456;
- 函式體內執行完畢,第4行,給外層函式的區域性變數a賦值為10
- 第5行,a=10;
- 第6行,由於在函式體內賦值了一個全域性變數c=456,函式執行完並沒有銷燬,所以這裡:c=456;
最終結果
彩蛋
通過以上講解,中間穿插了一些基礎知識,這裡跟大家簡要總結分享下。
函式的定義
函式的定義有三種形式:
-
函式申明
function fun1(){}; 複製程式碼
-
函式表示式
var fun2 = function(){}; 複製程式碼
-
建構函式Function
var fun3 = new Function("a", "b", "return a * b"); 複製程式碼
變數提升(Hoisting)
就是將函式體內的 區域性變數
和 函式申明
放到函式的最前面定義。
連等表示式
請移步: JS解惑-連等表示式
什麼是形參、實參
function fun4(var1,var2){//函式結構體括號內的變數,就叫做形參。
//TODO
};
fun4('abc',123);//呼叫函式時,實際傳的值就叫做實參。
複製程式碼
GO和VO物件是什麼
- VO,Variable Object,變數物件。這是一個偽物件,是用在函式體內,在函式
準備
階段時,用來存放準備的資料的,這些資料包括3類:形參、變數和函式宣告。- VO物件,不能直接在函式中訪問,因為其實它只是一種說法而已,用來表示資料的儲存行為的。
- 所以,上文中就沒提這個概念,你要用的時候,直接就訪問函式的變數了,跟這個物件沒啥關係,大家也可以不用太關心,只需要瞭解即可。
- GO,Global Object,全域性物件。這個概念是跟VO有點對立,就是全域性存在的一個物件,這個物件一般指的是
window
物件。
參考
(全文完)