本文不進行系統的講解,只枚舉及個特殊的例子。
例1
<script>
var str1 = "hello";
var str2 = "world";
function t1() {
console.log(str1);
console.log(str2);
var str2 = "toby";
console.log(str2);
}
//這裏會輸出什麼?
t1();
</script>
結果:
- hello
- undefined
- tbody
你一定奇怪爲什麼是undefined,因爲根據函數ti來說,他是有自己的str2變量的,只不過聲明在試用之後,所以是undefined。
例2
<script>
function t(userName) {
console.log(userName);//這裏輸出什麼?
function userName() {
console.log('tom');
}
}
t('toby');
</script>
你一定以爲這裏輸出的是tbody,但實際處出結果是:
function userName() {
console.log('tom');
}
執行t(‘toby’)的時候,會開始兩個階段,一個是分析階段,分析完就到執行階段
分析階段:
函數運行的瞬間,會生成一個Active Object對象(以下簡稱AO對象),一個函數作用域內能找到的所有變量,都在AO上,此時用代碼表示爲: t.AO = {}
分析參數: 接收參數,以參數名爲屬性,參數值爲屬性值,因爲沒有參數,因此分析結果用代碼表示爲: t.AO = {userName : toby}
分析var聲明: t函數內沒有var聲明,略過
分析函數聲明: 這個函數聲明有個特點,AO上如果有與函數名同名的屬性,則會被此函數覆蓋,因爲函數在JS領域,也是變量的一種類型,因此用代碼表示爲: t.AO = { userName : function userName() {console.log(‘tom’);}}
執行階段:
執行t(‘toby’)的時候,當執行到console.log(userName)時,就調用t.AO.userName,所以,最後的輸出結果是function userName() {console.log(‘tom’);}
所以重點就在於,同一域內,同名的函數和變量,函數優先,也就是函數會覆蓋變量。
例3:
<script>
function t(userName) {
console.log(userName);//這裏輸出什麼?
var userName = function () {
console.log('tom');
}
console.log(userName);//這裏輸出什麼
}
t('toby');
</script>
結果:
- tbody
- function () {
console.log(‘tom’);
}
分析階段:
創建AO對象,t.AO = {}
分析參數: t.AO = {userName : toby}
分析var聲明: 在AO上,形成一個屬性,以var的變量名爲屬性名,值爲undefined,(因爲是先分析,後執行,這只是詞法分析階段,並不是執行階段,分析階段值都是undefined,如果執行階段有賦值操作,那值會按照正常賦值改變),也就是說代碼應該表示爲:t.AO = {userName : undefined},但是還有另外一個原則,那就是如果AO有已經有同名屬性,則不影響(也就是什麼事都不做),由於分析參數時,AO上已經有userName這個屬性了,所以按照這個原則,此時什麼事都不做,也就是說,此時按照分析參數時的結果t.AO = {userName : toby}
分析函數聲明: 此時沒有函數聲明,略過
執行階段:
調用t.AO.userName,所以,最後的輸出結果是toby
例4:
<script>
t();
t2();
function t() {
console.log('toby');//這裏會輸出什麼?
}
var t2 = function () {
console.log('hello toby');//這裏會輸出什麼?
};
</script>
結果:tbody 報錯。因爲第一個已解析的時候已經將整個函數提升到頂部,但是第二個相當於變量,只有聲明,沒有定義。
例5:
<script>
function t(userName) {
console.log(userName);//這裏輸出什麼?
function userName() {
console.log(userName);//這裏輸出什麼?
}
userName();
}
t('toby');
</script>
結果:
- function userName() {console.log(userName);}}
- function userName() {console.log(userName);}}
t(‘toby’)的分析和執行階段
分析階段:
創建AO對象,t.AO = {}
分析參數: t.AO = {userName : toby}
分析var聲明: 有同名屬性,不做任何事,還是t.AO = {userName : toby}
分析函數聲明: 有同名屬性,覆蓋: t.AO = {userName : function userName() {console.log(userName);}}
執行階段:
t.AO.userName 輸出爲function userName() {console.log(userName);}}
userName()的分析和執行階段
這裏也要搞清楚兩個概念
//執行userName()分析的是
function () {
console.log(userName);
};
//而不是
var userName = function () {
console.log(userName);
};
分析階段:
創建AO對象,userName.AO = {}
分析參數: 無,略過
分析var聲明: 無,略過
分析函數聲明: 無,略過
執行階段:
因爲此時userName.AO = {}是個空對象,無法執行userName.AO.userName,所以會向上一層找,所以輸出t.AO.userName的結果,也就是function userName() {console.log(userName);}}
例6:
<script>
function t(userName) {
console.log(userName);//這裏輸出什麼?
var userName = function () {
console.log(userName);//這裏輸出什麼?
}
userName();
}
t('toby');
</script>
結果:
- toby
- function () {
console.log(userName);//這裏輸出什麼?
}
t(‘toby’)的分析和執行階段
分析階段:
創建AO對象,t.AO = {}
分析參數: t.AO = {userName : toby}
分析var聲明: 有同名屬性,不做任何事,還是t.AO = {userName : toby}
分析函數聲明: 無,略過
執行階段:
執行console.log(userName);時調用t.AO.userName 輸出爲toby,執行完後,代碼繼續往下執行,那麼就到了進行var的賦值操作(var的分析和執行的區別看例子2中我有解釋),此時t.AO = {userName : function userName() {console.log(userName);}}},代碼繼續往下執行,接着就執行到了userName()
userName()的分析和執行階段
分析階段:
創建AO對象,userName.AO = {}
分析參數: 無,略過
分析var聲明: 無,略過
分析函數聲明: 無,略過
執行階段:
按照例子4我們知道userName.AO是個空對象,所以會往上調用t.AO.userName,所以輸出爲:function userName() {console.log(userName);}}}
總結:
後面的變量與前面的變量同名,如果後面變量的值是undefined,則不會覆蓋前面的,也就是隻有執行到了才賦值,賦了值才能覆蓋。也就是說,用這個同名變量時,看之前誰最後一次賦的值,就是那個值。(賦值爲undefined也算賦值)
var x=1;
console.log(x);//1
var x;
console.log(x);//1(這樣未賦值的undefine就覆蓋不了之前的,如果這裏給x=undefined,可以覆蓋,輸出結果就爲undefined)
var x=3;
console.log(x);//3
JavaScript作用域會先在自己的AO上找,找不到就到父函數的AO上找,再找不到再找上一層的AO,直到找到window.這樣就形成一條鏈,這條AO鏈就是JavaScript中的作用域鏈.JavaScript中有兩條很重要的鏈,一條是作用域鏈,一條是原型鏈。