當前位置:才華齋>設計>網頁設計>

javascript作用域和閉包的深入理解

網頁設計 閱讀(1.6W)

作用域

javascript作用域和閉包的深入理解

作用域是一個變數和函式的作用範圍,javascript中函式內宣告的所有變數在函式體內始終是可見的,在javascript中有全域性作用域和區域性作用域,但是沒有塊級作用域,區域性變數的優先順序高於全域性變數,通過幾個示例來了解下javascript中作用域的那些“潛規則”(這些也是在前端面試中經常問到的問題)。

1. 變數宣告提前

示例1:

var scope="global";function scopeTest(){ (scope); var scope="local" }scopeTest(); //undefined

此處的輸出是undefined,並沒有報錯,這是因為在前面我們提到的函式內的宣告在函式體內始終可見,上面的函式等效於:

var scope="global";function scopeTest(){ var scope; (scope); scope="local" }scopeTest(); //local

注意,如果忘記var,那麼變數就被宣告為全域性變量了。

2. 沒有塊級作用域

和其他我們常用的語言不同,在Javascript中沒有塊級作用域:

function scopeTest() { var scope = {}; if (scope instanceof Object) { var j = 1; for (var i = 0; i < 10; i++) { //(i); } (i); //輸出10 } (j);//輸出1}

在javascript中變數的作用範圍是函式級的,即在函式中所有的變數在整個函式中都有定義,這也帶來了一些我們稍不注意就會碰到的“潛規則”:

var scope = "hello";function scopeTest() { (scope);//① var scope = "no"; (scope);//②}

在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全域性變數的值啊,這地方不應該為hello嗎?其實,上面的程式碼等效於:

var scope = "hello";function scopeTest() { var scope; (scope);//① scope = "no"; (scope);//②}

宣告提前、全域性變數優先順序低於區域性變數,根據這兩條規則就不難理解為什麼輸出undefined了。

作用域鏈

在javascript中,每個函式都有自己的執行上下文環境,當代碼在這個環境中執行時,會建立變數物件的作用域鏈,作用域鏈是一個物件列表或物件鏈,它保證了變數物件的有序訪問。

作用域鏈的前端是當前程式碼執行環境的變數物件,常被稱之為“活躍物件”,變數的查詢會從第一個鏈的物件開始,如果物件中包含變數屬性,那麼就停止查詢,如果沒有就會繼續向上級作用域鏈查詢,直到找到全域性物件中:

作用域鏈的逐級查詢,也會影響到程式的效能,變數作用域鏈越長對效能影響越大,這也是我們儘量避免使用全域性變數的一個主要原因。

閉包基礎概念

作用域是理解閉包的一個前提,閉包是指在當前作用域內總是能訪問外部作用域中的變數。

function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } }}var builder = new createClosure();tr();(tr()); //rose:hello

上面的示例在函式中返回了兩個閉包,這兩個閉包都維持著對外部作用域的引用,因此不管在哪呼叫總是能夠訪問外部函式中的變數。在一個函式內部定義的函式,會將外部函式的活躍物件新增到自己的作用域鏈中,因此上面例項中通過內部函式能夠訪問外部函式的屬性,這也是javascript模擬私有變數的一種方式。

注意:由於閉包會額外的附帶函式的作用域(內部匿名函式攜帶外部函式的作用域),因此,閉包會比其它函式多佔用些記憶體空間,過度的使用可能會導致記憶體佔用的增加。

閉包中的變數

在使用閉包時,由於作用域鏈機制的影響,閉包只能取得內部函式的最後一個值,這引起的'一個副作用就是如果內部函式在一個迴圈中,那麼變數的值始終為最後一個值。

//該例項不太合理,有一定延遲因素,此處主要為了說明閉包迴圈中存在的問題 function timeManage() { for (var i = 0; i < 5; i++) { setTimeout(function() { (i); },1000) }; }

上面的程式並沒有按照我們預期的輸入1-5的數字,而是5次全部輸出了5。再來看一個示例:

function createClosure(){ var result = []; for (var i = 0; i < 5; i++) { result[i] = function(){ return i; } } return result;}

呼叫createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。通過以上兩個例子可以看出閉包在帶有迴圈的內部函式使用時存在的問題:因為每個函式的作用域鏈中都儲存著對外部函式(timeManage、createClosure)的活躍物件,因此,他們都引用著同一變數i,當外部函式返回時,此時的i值為5,所以內部的每個函式i的值也為5。

那麼如何解決這個問題呢?我們可以通過匿名包裹器(匿名自執行函式表示式)來強制返回預期的結果:

function timeManage() { for (var i = 0; i < 5; i++) { (function(num) { setTimeout(function() { (num); }, 1000); })(i); }}

或者在閉包匿名函式中再返回一個匿名函式賦值:

function timeManage() { for (var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { (e); } })(i), 1000) }}//timeManager();輸出1,2,3,4,5function createClosure() { var result = []; for (var i = 0; i < 5; i++) { result[i] = function(num) { return function() { (num); } }(i); } return result;}//createClosure()[1]()輸出1;createClosure()[2]()輸出2

無論是匿名包裹器還是通過巢狀匿名函式的方式,原理上都是由於函式是按值傳遞,因此會將變數i的值複製給實參num,在匿名函式的內部又建立了一個用於返回num的匿名函式,這樣每個函式都有了一個num的副本,互不影響了。

閉包中的this

在閉包中使用this時要特別注意,稍微不慎可能會引起問題。通常我們理解this物件是執行時基於函式繫結的,全域性函式中this物件就是window物件,而當函式作為物件中的一個方法呼叫時,this等於這個物件(TODO 關於this做一次整理)。由於匿名函式的作用域是全域性性的,因此閉包的this通常指向全域性物件window:

var scope = "global";var object = { scope:"local", getScope:function(){ return function(){ return e; } }}

呼叫cope()()返回值為global而不是我們預期的local,前面我們說過閉包中內部匿名函式會攜帶外部函式的作用域,那為什麼沒有取得外部函式的this呢?每個函式在被呼叫時,都會自動建立this和arguments,內部匿名函式在查詢時,搜尋到活躍物件中存在我們想要的變數,因此停止向外部函式中的查詢,也就永遠不可能直接訪問外部函式中的變量了。總之,在閉包中函式作為某個物件的方法呼叫時,要特別注意,該方法內部匿名函式的this指向的是全域性變數。

幸運的是我們可以很簡單的解決這個問題,只需要把外部函式作用域的this存放到一個閉包能訪問的變數裡面即可:

var scope = "global";var object = { scope:"local", getScope:function(){ var that = this; return function(){ return e; } }}cope()()返回值為local。

記憶體與效能

由於閉包中包含與函式執行期上下文相同的作用域鏈引用,因此,會產生一定的負面作用,當函式中活躍物件和執行期上下文銷燬時,由於必要仍存在對活躍物件的引用,導致活躍物件無法銷燬,這意味著閉包比普通函式佔用更多的記憶體空間,在IE瀏覽器下還可能會導致記憶體洩漏的問題,如下:

function bindEvent(){ var target = lementById("elem"); ick = function(){ (); } }

上面例子中匿名函式對外部物件target產生一個引用,只要是匿名函式存在,這個引用就不會消失,外部函式的target物件也不會被銷燬,這就產生了一個迴圈引用。解決方案是通過建立副本減少對外部變數的迴圈引用以及手動重置物件:

function bindEvent(){ var target = lementById("elem"); var name = ; ick = function(){ (name); } target = null; }

閉包中如果存在對外部變數的訪問,無疑增加了識別符號的查詢路徑,在一定的情況下,這也會造成效能方面的損失。解決此類問題的辦法我們前面也曾提到過:儘量將外部變數存入到區域性變數中,減少作用域鏈的查詢長度。

總結:閉包不是javascript獨有的特性,但是在javascript中有其獨特的表現形式,使用閉包我們可以在javascript中定義一些私有變數,甚至模仿出塊級作用域,但閉包在使用過程中,存在的問題我們也需要了解,這樣才能避免不必要問題的出現。