在任何編程語言中,函數的功能都是十分強大的,JavaScript也不例外。之前已經講解了函數的一些基本知識,諸如函數定義,函數執行和函數返回值等,今天就帶大家深入瞭解JavaScript中函數的原理及執行過程。 一 函數參數 1,聲明函數時可以添加參數,相當於在函數內部隱式的聲明瞭變數。它的學名叫形 ...
在任何編程語言中,函數的功能都是十分強大的,JavaScript也不例外。之前已經講解了函數的一些基本知識,諸如函數定義,函數執行和函數返回值等,今天就帶大家深入瞭解JavaScript中函數的原理及執行過程。
一 函數參數
1,聲明函數時可以添加參數,相當於在函數內部隱式的聲明瞭變數。它的學名叫形式參數,簡稱形參,在執行函數使實際傳遞的值叫實際參數,簡稱實參。
1 function add(a,b){ 2 return a+b; 3 } 4 add(1,2);//3 5 //a和b就是形參,1和2就是實參
2,JS中函數的參數不限制個數和數據類型,這意味著函數的形參和實參個數可以不相等,都可以是無限多個。
1 function add(a,b){ 2 return a + b; 3 } 4 add(1);//NaN 1+undefined 5 add(1,2,3);//3 忽略3 6 add(1,"3");//"13"
3,函數甚至可以沒有形參,函數的arguments屬性,以類數組的形式存儲了函數執行時的實參。
4,函數有一個length屬性,表示函數的形參個數。
1 function test(){ 2 console.log(arguments); 3 console.log(test.length); 4 } 5 test(1,2,3); 6 //[0:1,1:2,2:3,length:3,...] 7 //0
5,arguments中的實參和形參是相互綁定的,修改其中一個,另一個也會改變,但形參和實參實際是兩個變數。實際上實參列表的個數是不可更改的,函數執行時傳遞幾個就是幾個。
1 function test(a,b){ 2 console.log(arguments); 3 console.log(b); 4 b = 2; 5 console.log(arguments); 6 console.log(b); 7 a = 10; 8 console.log(arguments); 9 } 10 test(1); 11 /* 12 [0:1,length:1,...] 13 undefined 14 [0:1,length:1,...] 15 2 16 [0:10,length:1,...] 17 */
二 預編譯
前面介紹JavaScript時,提到它是單線程,解釋性語言,即讀到一行就執行一行。其實這隻是它的表象,實際上JavaScript執行代碼分為了3個大的步驟:
1, 語法分析
語法分析的工作大體就是檢測是否有語法錯誤,是否符合版本規則等等。如果沒有問題則進入預編譯階段,如果遇到問題則會拋出錯誤。
2, 預編譯
在理解預編譯之前,我們應該先明白兩個概念:
a:如果變數未申明即訪問將報錯,但是變數未聲明即賦值,那麼該變數將自動升級成全局對象(window)的屬性。
b:在全局申明的變數,也會自動升級成window的屬性。
1 console.log(a);//Reference error:a is not defined 2 ************************************************* 3 a = 10;//10 4 a === window.a;//true 5 var b; 6 b === window.b;//true
程式預編譯發生在即將執行之前,分為三步:
1) 創建一個GO(Global Object)對象(也稱為全局作用域),實際上就是window對象。
2) 查找變數聲明,並把他們作為GO對象的屬性,值為undefined。
3) 查找是否有函數聲明,若有,則把函數名作為GO對象的屬性,並把函數體賦值給該屬性,若函數名和變數名相同,則會覆蓋他們。
函數也有預編譯過程,函數的預編譯發生在函數即將執行之前,分為四步:
1) 創建一個AO(Active Object)對象(即執行期上下文,也叫函數作用域或本地作用域)。
2) 查找形參和變數聲明,並把他們作為AO對象的屬性,值為undefined。
3) 將實參賦值給形參。
4) 查找函數內部是否有函數聲明,若有,則把函數名作為AO對象的屬性,並把函數體賦值給該屬性,若函數名和形參或變數名相同,則會覆蓋他們。
總結下來,可以簡單概括為:變數聲明時,聲明提升。函數聲明時,整體提升。函數聲明優先順序大於變數和形參。
1 function fn(a) { 2 console.log(a); //ƒ a() {} 3 console.log(b); //undefined 4 console.log(c); //ƒ c() {} 5 var a = 123; 6 console.log(a); //123 7 function a() {} 8 console.log(a); //123 9 var b = function b() {}; 10 console.log(b); //ƒ b() {} 11 function c() {} 12 } 13 fn(1); 14 /* 15 GO --> {fn:fn} 16 17 AO 18 第一步:AO --> {} 19 第二步:AO --> {a:undefined,b:undefined} 20 第三步:AO --> {a:1,b:undefined} 21 第四步:AO --> {a:function a() {},b:undefined,c:function c() {}} 22 23 註意這裡只是預編譯過程,函數真正執行時,AO中的屬性值會動態改變。所以: 24 第一行代碼直接列印fn a 25 第二行列印undefined 26 第三行列印fn c 27 第四行聲明變數a已經被提前執行了,這裡直接賦值123 28 第五行列印123 29 第六行函數聲明被整體提前了,這一行代碼將被直接跳過 30 第七行依然列印123 31 第八行只執行賦值操作,b == fn b 32 第九行則列印fn b 33 第十行聲明函數已經被整體提升了 34 */
3, 解釋執行
根據預編譯後的代碼順序,一條一條的執行。
三 作用域和作用域鏈
每一個函數都有一個隱式的屬性[[scope]],它存儲的是函數在執行時創建的執行期上下文集合,即一堆AO和GO對象,當然他們是有順序的,類似一個數組,它只能被系統調用,而不能被我們訪問和使用。
當一個函數在全局被創建時(沒有被執行,這時還沒有產生它自身的AO對象),[[scope]]將被插入GO對象。當函數執行時(這時已經創建了自己的AO對象),[[scope]]的頭部將被插入一個自己的AO對象,類似數組的unshift()方法。現在[[scope]]中已經有兩個執行期上下文的對象了:第0位的AO,第1位的GO。
1 function fn(){} 2 //fn.[[scope]] --> {0:GO} 3 fn(); 4 //fn.[[scope]] --> {0:AO,1:GO}
如果全局函數的內部定義了一個子函數,那麼該子函數的[[scope]]屬性類似的會存儲:第0位父函數的AO,第1位GO(因為只有父函數執行才會產生子函數的定義,所以子函數被定義時就已經有兩個執行期上下文對象了),這時子函數還沒被執行,所以它只會存儲這兩個對象。當他被執行時,子函數[[scope]]屬性的頭部將被插入它自己的AO對象。如果子函數內部還定義的有其他函數,那麼它的[[scope]]屬性生成方式和上面相同。
1 function fn(){ 2 function son(){}; 3 } 4 //fn.[[scope]] --> {0:GO} 5 //son.[[scope]] --> {} fn還沒執行,son都還沒聲明 6 fn(); 7 //fn.[[scope]] --> {0:AO(fn),1:GO} 8 //son.[[scope]] --> {0:AO(fn),1:GO} 這裡只是聲明瞭函數son,所以並沒有AO(son),如果function son(){}後面還有一行代碼:son();那麼當執行到這一行時,son.[[scope]] --> {0:AO(son),1:AO(fn),2:GO}
這樣就形成了函數的作用域鏈。當我們在函數內部訪問變數時,實際上是在函數的[[scope]]屬性里依次查找(從第0位開始),直到全局GO(window)對象。函數作用域鏈的最終表現就是:子函數可以訪問父函數的變數,父函數不能訪問子函數的變數。
每次函數執行產生的執行期上下文都是獨一無二的,當函數執行完畢,它自己的AO將被永遠銷毀,並更新自己的[[scope]]屬性。下一次執行將產生一個新的AO對象,並添加到[[scope]]屬性中。
下一次將更新將介紹JavaScript函數中另外兩個重要的應用:立即執行函數和閉包。