關於javascript中變數範圍理解

來源:互聯網
上載者:User

現在就結合網上的一篇文章在重新回顧下範圍這個東西吧。 範圍和上下文並不是同一個東西,很多人可能會把它搞混。每一個函數調用都聯絡著一個範圍和一個上下文。根本上來說,範圍是基於函數的而上下文是基於對象的。換句話說,範圍與函數調用是能夠擷取的變數有聯絡,它對與每一次調用來說都是獨一無二的。上下文常常代表this變數的值,它指向“擁有”當前執行的這段代碼的對象。

變數範圍

一個變數的範圍是程式原始碼中定義這個變數的地區。全域變數擁有全域範圍,在js代碼裡的任何地方都是有定義的。然而在函數內聲明的變數只在函數體內有定義,他們是局部變數,範圍是局部性的,函數參數也是局部變數,他們只在函數體內有定義。在函數體內局部變數的優先順序高於同名的全域變數,如果在函數體內聲明的一個局部變數或者函數參數中帶有的變數和全域變數重名,那麼全域變數就被局部變數所遮蓋。

Javascript目前並不支援塊範圍,在其中你可以在一個if 聲明,switch聲明,for迴圈或者while迴圈中定義一個變數範圍。這意味著變數在開閉花括弧之外不能被擷取。目前來說,任何定義在塊內的變數都能在塊之外被擷取。然而,這樣的情況馬上就要改變了,為了支援定義塊範圍變數,let關鍵字已經被官方增加到了ES6的標準中.

“this”上下文

上下文經常決定一個函數是怎麼被調用的。當一個函數作為一個對象的方法被掉調用時,this指向調用這個方法的對象:

 代碼如下 複製代碼

var object = { foo: function(){ alert(this === object); } };

object.foo(); // true `

 同樣的原則也適用於當使用new操作符定義一個對象執行個體的情況。在這種情況下,在函數範圍內的this指向新建立的執行個體:

 代碼如下 複製代碼

function foo(){ alert(this); }

foo() // window new foo() // foo`

當作為未繫結物件被調用時,this預設指向全域上下文或者瀏覽器中的window對象。然而,如果函數在strict 模式下被執行,上下文將被預設為undefined

執行內容和範圍鏈

Javascript是一門單線程語言,這意味著在瀏覽器環境下一個時間點只能做一件事。當Javascript解譯器初始化執行代碼時,它首先預設進入一個全域執行內容。在此基礎上每一次函數的調用都將建立一個新的執行內容。

這裡通常就是產生疑惑的地方,這裡所說的“執行內容”實際上對應著所有指向範圍的意圖和目的,它於前面所討論的上下文有所不同。這是一個很不好的命名管理,但是它很不幸的是已經被定義到了ECMAScript的標準中,這實在是讓人有點無法接受。

每次一個新的執行內容被建立時,它都被添加到了範圍鏈(有時它也被稱為執行棧或者調用棧)的頂部。瀏覽器總是執行當前位於範圍鏈頂部的執行內容。一旦執行完成,它就會被從棧的頂部移除,並將控制權返回到它下面的執行內容。例如:

function first(){ second(); function second(){ third(); function third(){ fourth(); function fourth(){ // do something } } }
} first(); 運行上邊的代碼會導致嵌套函數一路執行一直到fourth函數。在這個點上的作用於連從上到下的順序是:fourth,third,second,first,global。fourth函數可以擷取到全域變數以及任何定義在first,second,third 中的變數以及函數。一旦fourth函數執行完成,它將會被從範圍中被移除,執行權將會返回到third函數。這個過程一直繼續直到所有的程式碼完成執行。

不同執行內容中的命名衝突將由範圍鏈的攀登(climbing up the scope chain)來解決,它從本地一直移動到全域。這意味著擁有相同名字並位於範圍鏈更上方的的本地變數會被優先擷取。

一個執行內容分為建立和執行兩個階段。在建立階段,解譯器首先建立一個變數對象(也被成為啟用物件),它由執行內容中定義的所有變數,函式宣告以及參數組成。從這裡開始接下來範圍連被初始化,this的值隨後被決定。接著在執行階段,代碼被解釋執行。

簡單來說,每次當你試圖擷取一個函數執行內容中的值是,查詢過程將總是從自己的變數對象開始。如果這個變數在變數對象中沒有被找到,搜尋將會轉向範圍鏈。它將會攀登範圍鏈來檢查每個執行內容,尋找是個否有名字匹配的變數。

閉包

當一個嵌套函數試圖擷取外部函數之外的值時,閉包便產生了,它將在外部函數返回之後被執行。它將保持對外部函數本地變數,以及在內部定義的函數的擷取能力。封裝允許我們在暴露一個公用介面的情況下隱藏和保持來自外部範圍的執行內容,並用於未來的操控。下面是一個簡單的例子:

 代碼如下 複製代碼

function foo(){ var local = 'private variable'; return function bar(){ return local; } }

var getLocalVariable = foo(); getLocalVariable() // private variable 一個最流行的閉包類型是廣為流傳的模組模式。它允許你類比公用,私人以及特權成員:

 代碼如下 複製代碼

var Module = (function(){ var privateProperty = 'foo';

function privateMethod(args){
    //do something
}
return {
    publicProperty: "",
    publicMethod: function(args){
        //do something
    },
    privilegedMethod: function(args){
        privateMethod(args);
    }
}
})();

這個模組執行的過程似乎是在編譯器解釋它之後作為一個單體被執行。在這個閉包的執行內容外部唯一可以擷取的成員是你返回對象中的屬性和方法(例如Module.publicMethod)。然而,由於執行內容被保護,所有的私人屬性和方法在應用的生命週期內都會保持活躍,這意味著所有變數在未來將可以通過共有方法被擷取。

另一個類型的閉包叫做立即執行函數運算式(IIFE),它僅僅是一個在window上下文中自我調用的匿名函數:

 代碼如下 複製代碼

function(window){

var a = 'foo', b = 'bar';
function private(){
    // do something
}
window.Module = {
    public: function(){
        // do something
    }
};
})(this);

當試圖保持全域命名空間時這個運算式非常有用,任何在這個函數體內生命的變數對於閉包來說都是本地變數,但是它們又將始終在運行期間保持活躍。這是一個為應用和架構封裝原始碼的好方法,尤其適用於暴露一個用於互動的全域介面的情形。

call和apply

這是兩個位於所有函數內部的簡單的方法,它們使你能在任何想要的上下文中執行任何函數。call函數要求參數顯式的一個一個羅列出來而apply要求你以數組的形式提供參數:

 代碼如下 複製代碼
function user(first, last, age){
// do something
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);

上面兩個函數調用的結果都相同,user函數都在window上下文中被調用並都給與的三個參數。

ECMAScript 5 引入了Function.prototype.bind方法用來操縱上下文。它返回了一個永久綁定在bind函數第一個參數上下文中的函數,而不管這個函數是怎麼使用的。它通過使用一個在合適的上下文中重新導向調用的閉包來實現。下面是在不支援bind的瀏覽器中的實現方法:

 代碼如下 複製代碼
if(!('bind' in Function.prototype)){ Function.prototype.bind = function(){ var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1); return function(){ return fn.apply(context, args); } } }`

這在上下文經常性丟失的情形下很常用:物件導向和事件處理。這很有必要因為一個節點的addEventListener方法總是在節時間點事件處理器被綁定的上下文中執行回呼函數,它也應該這樣做。然而如果你要使用更進階的物件導向技巧並不要一個對象的方法作為回呼函數,你需要去手動調整上下文。下面的是一個用到了bind函數的例子:

 代碼如下 複製代碼

function MyClass(){ this.element = document.createElement('div'); this.element.addEventListener('click', this.onClick.bind(this), false); }

MyClass.prototype.onClick = function(e){ // do something };

當你回看bind函數的代碼是,你可能會注意到其中一行代碼涉及了Array對象的一個方法:

`Array.prototype.slice.call(arguments, 1); 有意思的一點是這裡的arguments對象並不是真正的數組,然而它經常被描述為一個類數組的對象,辟穀期更像是一個節點列表(由document.getElemntsBytagName()返回的東西)。它們包含一個Length屬性和索引值但是它們任然不是數組,因此並不支援任何數組的原生方法例如slice和push。然而,由於它們太相似了,因此數組的方法能被採用或者說劫持。在上面的例子中,數組對象的方法都在一個類數組對象的上下文中被執行。

這種使用另一個對象方法的技巧也被運用於類比傳統繼承方法的Javascript中的物件導向編程:

 代碼如下 複製代碼
MyClass.prototype.init = function(){ //在"MyClass"執行個體的上下文中調用超類的init方法 MySuperClass.prototype.init.apply(this, arguments); }

通過在一個子類(MyClass)執行個體的上下文中調用超類(MySuperClass)的方法,我們能夠模仿這種強大的設計模式。

結論

在你開始學習更進階的設計模式之前,理解這些概念非常的重要,因為範圍和上下文在現代Javascript中邊沿了一個重要又基礎的角色。無論我們談論閉包,物件導向還是繼承,或者多種事件的實現,上下文和範圍鏈都扮演著一個非常重要的角色。如果你的目標是掌握Javascript語言並且更好的理解它,那麼範圍鏈和閉包應該是你學習的起點。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.