五個小例子教你搞懂 JavaScript 作用域問題


眾所周知,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

更多內容盡在這里:相關博客一覽表

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM