- 相關(guān)推薦
javascript作用域和閉包的深入理解
作用域
作用域是一個(gè)變量和函數的作用范圍,javascript中函數內聲明的所有變量在函數體內始終是可見(jiàn)的,在javascript中有全局作用域和局部作用域,但是沒(méi)有塊級作用域,局部變量的優(yōu)先級高于全局變量,通過(guò)幾個(gè)示例來(lái)了解下javascript中作用域的那些“潛規則”(這些也是在前端面試中經(jīng)常問(wèn)到的問(wèn)題)。
1. 變量聲明提前
示例1:
var scope="global";function scopeTest(){ console.log(scope); var scope="local" }scopeTest(); //undefined
此處的輸出是undefined,并沒(méi)有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見(jiàn),上面的函數等效于:
var scope="global";function scopeTest(){ var scope; console.log(scope); scope="local" }scopeTest(); //local
注意,如果忘記var,那么變量就被聲明為全局變量了。
2. 沒(méi)有塊級作用域
和其他我們常用的語(yǔ)言不同,在Javascript中沒(méi)有塊級作用域:
function scopeTest() { var scope = {}; if (scope instanceof Object) { var j = 1; for (var i = 0; i < 10; i++) { //console.log(i); } console.log(i); //輸出10 } console.log(j);//輸出1}
在javascript中變量的作用范圍是函數級的,即在函數中所有的變量在整個(gè)函數中都有定義,這也帶來(lái)了一些我們稍不注意就會(huì )碰到的“潛規則”:
var scope = "hello";function scopeTest() { console.log(scope);//① var scope = "no"; console.log(scope);//②}
在①處輸出的值竟然是undefined,簡(jiǎn)直喪心病狂啊,我們已經(jīng)定義了全局變量的值啊,這地方不應該為hello嗎?其實(shí),上面的代碼等效于:
var scope = "hello";function scopeTest() { var scope; console.log(scope);//① scope = "no"; console.log(scope);//②}
聲明提前、全局變量?jì)?yōu)先級低于局部變量,根據這兩條規則就不難理解為什么輸出undefined了。
作用域鏈
在javascript中,每個(gè)函數都有自己的執行上下文環(huán)境,當代碼在這個(gè)環(huán)境中執行時(shí),會(huì )創(chuàng )建變量對象的作用域鏈,作用域鏈是一個(gè)對象列表或對象鏈,它保證了變量對象的有序訪(fǎng)問(wèn)。
作用域鏈的前端是當前代碼執行環(huán)境的變量對象,常被稱(chēng)之為“活躍對象”,變量的查找會(huì )從第一個(gè)鏈的對象開(kāi)始,如果對象中包含變量屬性,那么就停止查找,如果沒(méi)有就會(huì )繼續向上級作用域鏈查找,直到找到全局對象中:
作用域鏈的逐級查找,也會(huì )影響到程序的性能,變量作用域鏈越長(cháng)對性能影響越大,這也是我們盡量避免使用全局變量的一個(gè)主要原因。
閉包基礎概念
作用域是理解閉包的一個(gè)前提,閉包是指在當前作用域內總是能訪(fǎng)問(wèn)外部作用域中的變量。
function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } }}var builder = new createClosure();builder.setStr();console.log(builder.getStr()); //rose:hello
上面的示例在函數中返回了兩個(gè)閉包,這兩個(gè)閉包都維持著(zhù)對外部作用域的引用,因此不管在哪調用總是能夠訪(fǎng)問(wèn)外部函數中的變量。在一個(gè)函數內部定義的函數,會(huì )將外部函數的活躍對象添加到自己的作用域鏈中,因此上面實(shí)例中通過(guò)內部函數能夠訪(fǎng)問(wèn)外部函數的屬性,這也是javascript模擬私有變量的一種方式。
注意:由于閉包會(huì )額外的附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會(huì )比其它函數多占用些內存空間,過(guò)度的使用可能會(huì )導致內存占用的增加。
閉包中的變量
在使用閉包時(shí),由于作用域鏈機制的影響,閉包只能取得內部函數的最后一個(gè)值,這引起的一個(gè)副作用就是如果內部函數在一個(gè)循環(huán)中,那么變量的值始終為最后一個(gè)值。
//該實(shí)例不太合理,有一定延遲因素,此處主要為了說(shuō)明閉包循環(huán)中存在的問(wèn)題 function timeManage() { for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); },1000) }; }
上面的程序并沒(méi)有按照我們預期的輸入1-5的數字,而是5次全部輸出了5。再來(lái)看一個(gè)示例:
function createClosure(){ var result = []; for (var i = 0; i < 5; i++) { result[i] = function(){ return i; } } return result;}
調用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。通過(guò)以上兩個(gè)例子可以看出閉包在帶有循環(huán)的內部函數使用時(shí)存在的問(wèn)題:因為每個(gè)函數的作用域鏈中都保存著(zhù)對外部函數(timeManage、createClosure)的活躍對象,因此,他們都引用著(zhù)同一變量i,當外部函數返回時(shí),此時(shí)的i值為5,所以?xún)炔康拿總(gè)函數i的值也為5。
那么如何解決這個(gè)問(wèn)題呢?我們可以通過(guò)匿名包裹器(匿名自執行函數表達式)來(lái)強制返回預期的結果:
function timeManage() { for (var i = 0; i < 5; i++) { (function(num) { setTimeout(function() { console.log(num); }, 1000); })(i); }}
或者在閉包匿名函數中再返回一個(gè)匿名函數賦值:
function timeManage() { for (var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { console.log(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() { console.log(num); } }(i); } return result;}//createClosure()[1]()輸出1;createClosure()[2]()輸出2
無(wú)論是匿名包裹器還是通過(guò)嵌套匿名函數的方式,原理上都是由于函數是按值傳遞,因此會(huì )將變量i的值復制給實(shí)參num,在匿名函數的內部又創(chuàng )建了一個(gè)用于返回num的匿名函數,這樣每個(gè)函數都有了一個(gè)num的副本,互不影響了。
閉包中的this
在閉包中使用this時(shí)要特別注意,稍微不慎可能會(huì )引起問(wèn)題。通常我們理解this對象是運行時(shí)基于函數綁定的,全局函數中this對象就是window對象,而當函數作為對象中的一個(gè)方法調用時(shí),this等于這個(gè)對象(TODO 關(guān)于this做一次整理)。由于匿名函數的作用域是全局性的,因此閉包的this通常指向全局對象window:
var scope = "global";var object = { scope:"local", getScope:function(){ return function(){ return this.scope; } }}
調用object.getScope()()返回值為global而不是我們預期的local,前面我們說(shuō)過(guò)閉包中內部匿名函數會(huì )攜帶外部函數的作用域,那為什么沒(méi)有取得外部函數的this呢?每個(gè)函數在被調用時(shí),都會(huì )自動(dòng)創(chuàng )建this和arguments,內部匿名函數在查找時(shí),搜索到活躍對象中存在我們想要的變量,因此停止向外部函數中的查找,也就永遠不可能直接訪(fǎng)問(wèn)外部函數中的變量了?傊,在閉包中函數作為某個(gè)對象的方法調用時(shí),要特別注意,該方法內部匿名函數的this指向的是全局變量。
幸運的是我們可以很簡(jiǎn)單的解決這個(gè)問(wèn)題,只需要把外部函數作用域的this存放到一個(gè)閉包能訪(fǎng)問(wèn)的變量里面即可:
var scope = "global";var object = { scope:"local", getScope:function(){ var that = this; return function(){ return that.scope; } }}object.getScope()()返回值為local。
內存與性能
由于閉包中包含與函數運行期上下文相同的作用域鏈引用,因此,會(huì )產(chǎn)生一定的負面作用,當函數中活躍對象和運行期上下文銷(xiāo)毀時(shí),由于必要仍存在對活躍對象的引用,導致活躍對象無(wú)法銷(xiāo)毀,這意味著(zhù)閉包比普通函數占用更多的內存空間,在IE瀏覽器下還可能會(huì )導致內存泄漏的問(wèn)題,如下:
function bindEvent(){ var target = document.getElementById("elem"); target.onclick = function(){ console.log(target.name); } }
上面例子中匿名函數對外部對象target產(chǎn)生一個(gè)引用,只要是匿名函數存在,這個(gè)引用就不會(huì )消失,外部函數的target對象也不會(huì )被銷(xiāo)毀,這就產(chǎn)生了一個(gè)循環(huán)引用。解決方案是通過(guò)創(chuàng )建target.name副本減少對外部變量的循環(huán)引用以及手動(dòng)重置對象:
function bindEvent(){ var target = document.getElementById("elem"); var name = target.name; target.onclick = function(){ console.log(name); } target = null; }
閉包中如果存在對外部變量的訪(fǎng)問(wèn),無(wú)疑增加了標識符的查找路徑,在一定的情況下,這也會(huì )造成性能方面的損失。解決此類(lèi)問(wèn)題的辦法我們前面也曾提到過(guò):盡量將外部變量存入到局部變量中,減少作用域鏈的查找長(cháng)度。
總結:閉包不是javascript獨有的特性,但是在javascript中有其獨特的表現形式,使用閉包我們可以在javascript中定義一些私有變量,甚至模仿出塊級作用域,但閉包在使用過(guò)程中,存在的問(wèn)題我們也需要了解,這樣才能避免不必要問(wèn)題的出現。
【javascript作用域和閉包的深入理解】相關(guān)文章:
javascript的閉包概念怎么理解06-15
javascript閉包的定義及應用實(shí)例分析08-25
有關(guān)深入理解JavaScript中的并行處理的介紹10-14
對javascript的理解08-08
javascript跨域訪(fǎng)問(wèn)的方法07-09
理解JavaScript原型鏈教程09-02