預處理:創建一個詞法環境(LexicalEnvironment,在後面簡寫為LE),掃描JS中的用聲明的方式聲明的函數,用var定義的變量並將它們加到預處理階段的詞法環境中去。
比如說下面的這段代碼:
var a = 1;//用var定義的變量,以賦值 var b;//用var定義的變量,未賦值 c = 3;//未定義,直接賦值 function d(){//用聲明的方式聲明的函數 console.log('hello'); } var e = function(){//函數表達式 console.log('world'); }
在預處理時它創建的詞法作用域可以這樣表示:
LE{ //此時的LE相當於window a:undefined b:undefined 沒有c d:對函數的一個引用 沒有e }
強調:1、預處理的函數必須是JS中用聲明的方式聲明的函數(不是函數表達式),看例子:
d(); e(); function d(){//用聲明的方式聲明的函數 console.log('hello'); } var e = function(){//函數表達式 console.log('world'); }
輸出結果分別是:hello;報錯e is not a function
2、是用var定義的變量,看例子:
console.log(a);//undefined console.log(b);//undefined console.log(c);//報錯:c is not defined var a = 1; var b; c = 3;
來看下面的代碼:你覺得輸出結果是什麼?
console.log(f); var f = 1; function f(){ console.log('foodoir'); }
console.log(f); function f(){ console.log('foodoir'); } var f = 1;
console.log(f); var f = 1; var f = 2;
console.log(f); function f(){ console.log('foodoir'); } function f(){ console.log('hello world'); }
你可能跟我開始一樣,覺得輸出的是foodoir,這樣你就錯了,你應該繼續看看前面講的預處理的問題。第一段代碼輸出的結果應該是:function f(){console.log('foodoir');}。
看到第二段代碼,你可能想都沒想就回答結果是1,並且你還告訴原因說javascript裡面的函數沒有傳統意義的重載。是的javascript裡面的函數是沒有重載,但是第二段代碼的運行結果仍然是:function f(){console.log('foodoir');}。(原因後面作解釋)
如果你還覺得第三段代碼的結果是2或者是1,那麼我建議你回到前面看看關於預處理的例子。第三段的結果為:undefined。
第四段代碼的結果為function f(){console.log('hello world');}
原因:處理函數聲明有沖突時,會覆蓋;處理變量聲明有沖突時,會忽略。在既有函數聲明又有變量聲明的時候,你可以跟我一樣像CSS中的權重那樣去理解,函數聲明的權重總是高一些,所以最終結果往往是指向函數聲明的引用。
來看下面的例子:
console.log(a); console.log(b); console.log(c); console.log(d); var a = 1; b = 2; console.log(b); function c(){ console.log('c'); } var d = function(){ console.log('d'); } console.log(d);
1、我們先分析全局預處理的情況,結果如下:
LE{ a:undefined 沒有b c:對函數的一個引用 d:undefined }
此時,我們可以得到前四行代碼得到的結果分別為:
undefined
報錯
function c(){console.log('c');
undefined
2、當執行完預處理後,代碼開始一步步被解析(將第二行報錯的代碼注釋掉)
在第6行代碼執行完,LE中的a的值變為1;
LE{ a:1 沒有b c:對函數的一個引用 d:undefined }
第7行代碼執行完,LE中就有了b的值(且b的值為2,此時b的值直接變為全局變量);
LE{ a:1 b:2 c:對函數的一個引用 d:undefined }
第10行代碼執行完,
LE{ a:1 b:2 c:指向函數 d:undefined }
第14行代碼執行完,此時
LE{ a:1 b:2 c:指向函數 d:指向函數 }
關於b變為全局變量的例子,我們在控制台中輸入window.b,可以得到b的結果為2。結果如圖:
補充:運用詞法的作用域,我們可以很好的解釋一個帶多個參數的函數只傳遞一個參數的例子。
function f(a,b){ } f(1);
它的詞法作用域可以這樣解釋:
LE{ a:1 b:undefined }
函數中的解析和執行過程的區別不是很大,但是函數中有個arguments我們需要注意一下,我們來看下面的例子:
function f(a,b){ alert(a); alert(b); var b = 100; function a(){} } f(1,2);
我們先來分析函數的預處理,它和全局的預處理類似,它的詞法結構如下:
LE{ b:2 a:指向函數的引用 arguments:2 } //arguments,調用函數時實際調用的參數個數
再結合之前的那句話:處理函數聲明有沖突時,會覆蓋;處理變量聲明時有沖突,會忽略。
故結果分別為:function a(){}和2
當傳入的參數值有一個時:
function f(a,b){ alert(a); alert(b); var b = 100; function a(){} } f(1);
這個時候的詞法結構如下:
LE{ b:undefined a:對函數的一個引用 arguments:1 }
故結果分別為:function a(){}和undefined
還有一個需要注意的地方有:如果沒有用var聲明的變量,會變成最外部LE的成員,即全局變量
function a(){ function b(){ g = 12; } b(); } a(); console.log(g);//12
控制台結果:
有了前面的基礎,我們就可以對JS的作用域和作用域鏈進行深入的了解了。
console.log(a);//undefined console.log(b);//undefined console.log(c);//c is not defined console.log(d);//d is not defined var a = 1; if(false){ var b = 2; }else{ c = 3; } function f(){ var d = 4; }
有了前面的基礎我們很容易就可以得到前三個的結果,但是對於第四個卻很是有疑問,這個時候,你就有必要看一看關於javascript作用域的相關知識了。
在編程語言中,作用域一般可以分為四類:塊級作用域、函數作用域、動態作用域、詞法作用域(也稱靜態作用域)
在其它C類語言中,用大括號括起來的部分被稱為作用域,但是在javascript並沒有塊級作用域,來看下面一個例子:
for(var i=0;i<3;i++){ // } console.log(i);
它的結果為3,原因:執行完for循環後,此時的i的值為3,在後面仍有效
沒有純粹的函數的作用域
來看下面的例子:
function f(){ alert(x); } function f1(){ var x = 1; f(); } function f2(){ var x = 1; f(); } f1(); f2();
如果說存在動態作用域,那麼結果應該是分別為1、2,但是最終結果並不是我們想要的,它的結果為:x is not defined。所以javascript也沒有動態作用域
我們可以在函數最前面聲明一個x=100
var x=100; function f(){ alert(x); } function f1(){ var x = 1; f(); } function f2(){ var x = 1; f(); } f1(); f2();
結果為分別彈出兩次100。說明javascript的作用域為靜態作用域 ,分析:
function f(){ alert(x); } // f [[scope]] == LE == window //創建一個作用域對象f [[scope]],它等於創建它時候的詞法環境LE(據前面的知識我們又可以知道此時的詞法環境等於window) function f1(){ var x = 1; f();//真正執行的時候(一步一步往上找)LE ->f.[[scope]] == window }
在詞法解析階段,就已經確定了相關的作用域。作用域還會形成一個相關的鏈條,我們稱之為作用域鏈。來看下面的例子:
function f(){ //f.scope == window var x = 100;//f.LE == {x:100,g:函數} var g = function(){//g.scope = f.LE alert(x); } g();//在執行g的時候,先找g.scope,沒有的話再找f.LE,還沒有的話找f.scope……一直往上找window } f();
最終結果為:100
來看一個經典的例子:
//定義全局變量color,對於全局都適用,即在任何地方都可以使用全局變量color var color = "red"; function changeColor(){ //在changeColor()函數內部定義局部變量anotherColor,只在函數changeColor()裡面有效 var anotherColor = "blue"; function swapColor(){ //在swapColor()函數內部定義局部變量tempColor,只在函數swapColor()裡面有效 var tempColor = anotherColor; anotherColor = color; color = tempColor; //這裡可以訪問color、anotherColor和tempColor console.log(color); //blue console.log(anotherColor); //red console.log(tempColor); //blue } swapColor(); //這裡只能訪問color,不能訪問anotherColor、tempColor console.log(color); //blue console.log(anotherColor); //anotherColor is not defined console.log(tempColor); //tempColor is not defined } changeColor(); //這裡只能訪問color console.log(color); //blue console.log(anotherColor); //anotherColor is not defined console.log(tempColor); //tempColor is not defined
還需要注意的是:new Function的情況又不一樣
var x= 123; function f(){ var x = 100; //g.[[scope]] == window var g = new Function("","alert(x)"); g(); } f(); //結果為:123
小結:
以f1{ f2{ x}}為例,想得到x,首先會在函數裡面的詞法環境裡面去找,還沒找到去父級函數的詞法環境裡面去找……一直到window對象裡面去找。
這時候,問題來了。。。。
問題1:到這裡看來如果有多個函數都想要一個變量,每次都要寫一個好麻煩啊,我們有什麼方法可以偷懶沒?
方法:將變量設置為全局變量
問題2:不是說要減少全局用量的使用麼?因為在我們做大項目的時候難免要引入多個JS庫,變量間的命名可能會有沖突,且出錯後不易查找,這個時候我們該怎麼辦呢?
方法:將變量設置在一個打的function中,比如下面這樣:
function(){ var a = 1; var b = 2; function f(){ alert(a); } }
問題3:照你的這種方法我們在外面又訪問不到了,怎麼辦?
方法:我們使用匿名函數的方法,示例如下:
(function(){ var a = 1, b = 2; function f(){ alert(a); } window.f = f; })(); f(); //結果為:1