眾所周知,JavaScript 的作用域和其他傳統語言(類C)差別比較大,掌握並熟練運用JavaScript 的作用域知識,不僅有利於我們閱讀理解別人的代碼,也有助於我們編寫自己的可靠代碼。
下面筆者將使用五個小例子來給大家分析下 JavaScript 的作用域要注意的問題。
感謝 例子的來源 (這5個例子我做錯了2個 [嘿嘿,盡情鄙視吧],筆者就是要 死磕自己,奉獻大家!)
先給出五個例子:
每個例子旁邊都會給出答案的鏈接,如果你全部都正確了,你可以忽略這篇短文,並深深的鄙視下筆者。
例一: 答案
if (!("a" in window)) { var a = 1; } alert (a);
例二:答案
var a = 1, b = function a (x) { x && a (--x); }; alert (a);
例三:答案
function a (x) { return x * 2; } var a; alert (a);
例四:答案
function b (x, y, a) { arguments[2] = 10; alert (a); } b(1, 2, 3);
例五:答案
function a () { alert (this); } a.call (null);
寫在答案前面的話:
頁面中JavaScript代碼在加載的時候,執行順序是按照腳本標簽<script>的順序一致的,但如果設置該標簽async或defer屬性的話,則不能保證執行順序(這點說起來慚愧,筆者沒有認真測試過)。
JS代碼在解釋執行前,會對進行一次“預編譯”:
在預編譯的過程中,用var聲明的變量被設置為活動對象(啥是活動對象?)的屬性,默認值為“undefined”,
以function定義的函數也被添加為活動對象的屬性,它們的值就是函數的定義,匿名函數將不被解析(這句話啥意思?)。
變量初始化過程即賦值過程發生在解釋執行期,而不是"預編譯期"。
例一答案:
有人大概會犯下面兩種情況的錯誤:
情況一:if 分支里聲明a變量(var a = 1;),在if 外訪問不到變量a,所以對話框彈出 'undefined'。
這說明你對JavaScript 中沒有塊級作用域不太理解。請翻翻基礎書籍。筆者也會在后續的博文中 深入淺出地介紹JavaScript 變量、作用域和內存問題。(到時候會給出鏈接的)
情況二:a 變量,不在window對象中,所以進入if 分支,聲明 a 並賦值為 1,又由於JS沒有塊作用域,所以對話框彈出 1。
這說明你大概了解塊作用域(可能只是知道,但並不知道原理)。這時候你可能需要了解下啥是作用域鏈,多問問為什么沒有塊作用域(后續博文會推出的,但筆者仍希望你通過讀書的方式了解下)。
但是你還是對JS代碼執行前的情況不太了解。
真正的情況是這樣的:
JS在預編譯的時候,var 聲明的變量 被設置為活動對象(本例為 window )的屬性,默認值是‘undefined’,
由於沒有塊作用域,所以if 塊中的 變量聲明被預編譯了,因此 a 是window的屬性 (a in window is true ) ,於是就能理解對話框彈出 'undefined'.
例二答案:
錯誤的情況我就不多做介紹了,無非是彈出函數b的定義,或者彈出1。
下面解釋下本例的情況,本例的代碼執行和下列代碼執行是一樣的:
var a = 1; var b = function a (x) { x && a (--x); }; alert (a);
第一行是一個變量的聲明。
第二行是函數字面量(函數表達式,詳細用法請參見:深入淺出 JavaScript 函數 v 0.5),只不過該表達式沒有省略函數名(a),為什么不省略呢? 因為該函數要遞歸啊,不然咋遞歸?
但是殘酷的是,函數名在函數外部是未定義的,所以對話框彈出的是 1 。
針對本例還有一種說法是 逗號操作符,不知道是順序的還是倒序的,但是針對本例,順序還是逆序,真沒什么關系。
例三答案:
本例錯誤的大部分情況都是彈出'undefined'.
錯誤的原因就是不太了解JS的預編譯過程。
本例中JS的預編譯過程是這樣的,首先聲明變量 a (並未初始化喲),然后再初始化為function, 后面 var a ; 只是聲明變量,但是並未給a 賦值,所以其值還是function。
拿下面一段代碼做比較,可以印證上面的解釋:
function a (x) { return x * 2; } var a = 10; alert (a);
誰最后對同一個變量初始化(可以理解成賦值),最后變量就保留誰的值。
例四答案:
理解本例的關鍵在於對參數對象的理解,arguments 的詳細介紹,在深入淺出 JavaScript 函數 v 0.5中有詳細的介紹。
arguments 是一個特殊的對象,有數組的特性,但不是數組,arguments 對象不是只讀的,arguments [2] = 10;
這句話就把參數 a (其實可以理解成是函數的內部變量) 更改為10,所以彈出 10。
arguments [2] 和 a 指向的是同一個值。
例五答案:
a 作為一個函數,在JS中函數也是對象,對象當然有屬性和方法了。
JavaScript 就為函數對象提供了兩個間接調用函數的方法 call() 和apply(),這兩個內容的詳細解釋在深入淺出 JavaScript 函數 v 0.5中有詳細的介紹。
call () 方法的語法是這樣的:
call([thisObj[,arg1[, arg2[, [,.argN]]]]]) // thisObj 是this要綁定的對象,后面是逗號分隔開的參數
第一個參數是函數要執行的作用域(也就是說第一個參數都會變為this值),哪怕傳入的參數是原始值或者null或undefined,在非嚴格模式下,傳入的null和undefined都會被替換成全局對象,其他的原始值則會被響應的包裝對象所替代。本例中傳入null ,所以會替換成全局對象(window對象),因此對話框彈出 [Object Window] 。
本例中涉及的 this 的用法,請參見深入淺出 JavaScript 函數 v 0.5 。
寫在后面的話:
當函數被調用,活動對象(activation object) 就被創建了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具有索引屬性的參數映射表)。
活動對象在函數上下文中作為變量對象使用。
一句話,函數聲明在"預編譯階段"被解析,函數字面量(函數表達式) 在執行階段被解析。
例子:
alert (add (2,3)); //5 function add(a,b) { return a+b; } // 函數聲明提升 //=====為了方便,筆者寫在了一起,在測試的時候,可不要在一個作用域中執行喲=============== alert (add (2,3)); //error var add = function (a,b) { return a+b; }; //函數字面量,注意結尾的分號喲(細節很重要)。
廣了個告::(祝大家勞動節快樂,為我們這些勞動者鼓掌)
更過關於函數的內容,盡在 深入淺出 JavaScript 函數 v 0.5
更多內容盡在這里:相關博客一覽表