JS中的作用域及閉包
1、JS中的作用域
在 es6 出現之前JS中只有全局作用域和函數作用域,沒有塊級作用域,即 JS 在函數體內有自己的作用域,但是如果不是在函數體的話就全部都是全局作用域。比如在 if、for 等有 {} 的結構體,就不會具備自己的作用域,在這些結構體內聲明的變量將會是全局變量。由此可能導致一些問題,下面代碼示例:
var tmp = new Date(); function f() { console.log(tmp); if (false) { //即使沒有運行到下面的代碼,該變量聲明也被提升了。因為變量聲明是在編譯階段就被運行的,而不是代碼運行階段 var tmp = 'hello world'; } } f(); // undefined 由于變量提升,導致內層的tmp變量覆蓋了外層的tmp變量
var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5 用來計數的循環變量泄露為全局變量。
在 es6 中引入的塊級作用域就解決了這些問題。通過 let、const 引入塊級作用域后:
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 無法訪問到塊級作用域里的變量 }
1.1、函數內部變量的作用域取決于函數聲明的位置
編程語言的作用域規則有動態作用域和詞法作用域,詞法作用域的指的是函數和變量的作用域由聲明時所處的位置決定,JS使用的就是詞法作用域。
在JS中,調用函數時,函數內部的變量的作用域由函數聲明時所處的位置決定,而不是調用的位置。
var a = 'window' var f = function (){ console.log(a); } var b = function (){ var a = 'else' f(); } b(); //輸出 'window'
但是 JS 中的 this 指針并不遵守詞法作用域,而是取決于函數的調用方式。
1.2、塊級作用域
先看下面代碼:
var liList = ul.getElementsByTagName('li') for(var i=0; i<6; i++){ liList[i].onclick = function(){ alert(i) // 為什么 alert 出來的總是 6,而不是 0、1、2、3、4、5 } }
上面代碼可以看做是:
{ var i = 0; liList[i].onclick = function(){ alert(i) } } { var i = 1; liList[i].onclick = function(){ alert(i) } } ... { var i = 5; liList[i].onclick = function(){ alert(i) } } i = 6;
因為 javascript 沒有塊級作用域,作用每次 onclick 訪問 i 時,因為沒有塊級作用域的 i,所以只能查找全局作用域的 i,此時全局 i 是6,所以每次輸出 6。
而如果把 i 換成是 let 聲明,此時就有塊級作用域,每次訪問的 i 都是塊級作用域的 i,就能依次輸出正確結果。
2、全局變量和局部變量
JS中的變量只有兩種:全局變量和局部變量。函數體外聲明的變量,稱為全局變量。 函數內部使用 var 聲明的變量,稱為局部變量。
(網上都說在函數體內聲明的變量稱為局部變量,那塊級作用域中的變量是什么變量?我覺得應該是所有在 {} 結構體內聲明的變量都稱之為局部變量)
//函數內部可以直接讀取全局變量。 var n=999; function f1(){ alert(n); } f1(); // 999 //但是在函數外部無法讀取函數內的局部變量,因為函數內部的變量在函數執行完畢以后就會被釋放掉 function f1(){ var n=999; }
f1(); alert(n); //報錯: n is not defined
注意:在函數內部聲明變量的時候,一定要使用 var、let、const 命令。否則的話實際上是聲明了一個全局變量!
function f1(){ n=999; } f1(); alert(n); // 999
而要想訪問到局部變量,那么就可以使用閉包。
3、閉包的概念
閉包就是能夠讀取其他函數內部變量的函數。
在 es6之前,JavaScript中的變量的作用域只有兩種:全局作用域和函數作用域。函數內部可以直接讀取全局變量,但是在函數外部就無法讀取函數內的局部變量。要想從外部讀取函數內的局部變量就要用到閉包。
function f1() { n = 999; function f2() { alert(n); } return f2; }
var result = f1(); result(); // 999 console.log(n); //報錯 n is not defined
關于閉包的定義有很多種說法,有定義指閉包是外層函數,也有說閉包是內層函數。
還有一種說法,即指閉包不是某個函數,而是一項技術或者一個特性。函數作用域中的變量在函數執行完成之后就會被垃圾回收,一般情況下訪問一個函數作用域中的變量,正常是無法訪問的,只能通過特殊的技術或者特性來實現,就是在函數作用域中創建內部函數來實現,這樣就不會使得函數執行完成變量被回收,這種技術或者特性應該被稱為“閉包”,像是《JavaScript權威指南》打的比方,像是把變量包裹了起來,形象的稱為“閉包”。
閉包可以形象地理解成:將一個變量包裹起來了,在函數外部也可以訪問該變量。但請注意,閉包只是讓我們在函數外部可以使用另一個函數來訪問前一個函數內部的變量,但是該變量并不是變成了全局變量,直接訪問該變量會報錯。
4、閉包的作用
閉包可以用在許多地方。它的最大用處有兩個:(1)可以讀取函數內部的變量、(2)讓這些變量的值始終保持在內存中。
4.1、讓這些變量的值始終保持在內存中
正常來說,當一個函數執行完畢后,函數內的所有變量都會被回收,當函數再次執行時才會再次創建變量。但是,當函數完畢后,函數內的變量仍然在被其他函數使用時(實際上此時也就形成了閉包),則該變量不會被回收,將始終保持在內存中。
function f1() { var n = 999; nAdd = function () { n += 1 } function f2() { alert(n); } return f2; }
var result = f1(); result(); // 999 nAdd(); result(); // 1000
在上面的代碼中,f1 函數執行完畢后,n 變量正常來說是應該被銷毀的,但是由于 n 變量仍然被函數 nAdd 和 f2 引用,所以該變量不會被銷毀,將始終存在于內存中。
result 實際上就是閉包 f2 函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1中的局部變量n一直保存在內存中,并沒有在f1調用后被自動清除。(如果n是一個需要很費時的操作才能得到的值的話就會作用明顯,而且可以把局部變量駐留在內存中,避免使用全局變量)
這段代碼中另一個值得注意的地方,就是 "nAdd=function(){n+=1}" 這一行,nAdd 是一個全局變量,這個函數本身也是一個閉包,因為該函數的存在我們可以在函數外部對函數內部的局部變量進行操作。
4.2、減少全局變量的污染
所有的變量聲明時如果不加上var等關鍵字,則默認的會添加到全局對象的屬性上去,這樣的臨時變量加入全局對象有很多壞處,比如:別的函數可能誤用這些變量、造成全局對象過于龐大,影響訪問速度(因為變量的取值是需要從原型鏈上遍歷的)。
閉包有一個作用就是能減少全局變量的使用,因為使用閉包可以在外部訪問函數內部的變量,而該變量也只能通過函數訪問,并不是一個全局變量。
4.3、實現封裝私有屬性
var person = function () { //變量作用域為函數內部,外部無法訪問 var name = "default"; return { getName: function () { return name; }, setName: function (newName) { name = newName; } } }(); console.log(person.name); //直接訪問,結果為undefined console.log(person.getName()); // default person.setName("abruzzi"); console.log(person.getName()); // abruzzi
上面的代碼給對象 person 創建了私有變量 name,又對外提供了獲取的方法,對外封裝了一個對象并且不可直接訪問和操作,只可通過對象定義的方法進行操作,增加了安全性。
上面的代碼就是典型的自執行函數和閉包結合使用的示例,立即執行函數和閉包其實并沒有什么關系,只是兩者會經常結合在一起使用而已,但兩者有本質上的區別。兩者也有一個共同的優點就是能減少全局變量的使用。
4.4、實現繼承
下面定義了Person,它就像一個類,我們new一個 Person 對象,訪問它的方法。下面我們定義了Jack,繼承自 Person,并添加自己的方法,Jack 繼承了 Person。
function Person2() { var name = "default"; this.age = 12; return { getName: function () { return name; }, setName: function (newName) { name = newName; } } }; var p = new Person2(); console.log(p.age); p.setName("Tom"); console.log(p.getName()); var Jack = function () {}; //繼承自Person Jack.prototype = new Person2(); //添加私有方法 Jack.prototype.Say = function () { console.log("Hello,my name is Jack"); }; var j = new Jack(); j.setName("Jack"); j.Say(); console.log(j.getName());
4.5、閉包在實際開發中的使用
閉包在實際開發中的一般都是用來替代全局變量,避免造成變量污染。
function isFirstLoad(){ var list=[]; return function(option){ if(list.indexOf(option)>=0){ //檢測是否存在于現有數組中,有則說明已存在 console.log('已存在') }else{ list.push(option); console.log('首次傳入'); //沒有則返回true,并把這次的數據錄入進去 } } } var ifl=isFirstLoad(); ifl("zhangsan"); //首次傳入 ifl("lisi"); //首次傳入 ifl("zhangsan"); //已存在
可以看到,如果外界想訪問 list 變量,只能通過我定義的函數isFirstLoad來進行訪問。我對想訪問 list 的外界只提供了 isFirstLoad 這一個接口。至于怎么操作_list,我已經定義好了,外界能做的就只是使用我的函數,然后傳幾個不同的參數罷了。并且 list 變量并不是全局變量,所以就避免了變量污染。
閉包的用處在一道面試題中常常能看到:在 setTimeout 中依次輸出 0 1 2 3 4
//下面的代碼全部輸出 5 for (var i=1; i<5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); } //下面會依次輸出 0 1 2 3 4 for (var i=1; i<5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i*1000 ); })(i) }
上面的第二個循環中,在外層的 function 里面還包含著 setTimeout 里面的 function 函數,而里面的 function 函數就訪問了外層 function 的 i 的值,由此就形成了一個閉包。每次循環時,將 i 的值保存在一個閉包中,當 setTimeout 中定義的操作執行時,就會訪問對應閉包保存的 i 值,所以輸出 0 1 2 3 4。
循環為元素數組綁定事件:
// DOM操作 let li = document.querySelectorAll('li'); for(var i = 0; i < li.length; i++) { (function(i){ li[i].onclick = function() { alert(i); } })(i) }
5、閉包的危害
5.1、可能導致內存泄露
由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露(即己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,將導致程序運行速度減慢)。解決方法是,在退出函數之前,將外部的引用置為 null。
5.2、可能不小心修改掉私有屬性
閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象使用,把閉包當作它的公用方法,把內部變量當作它的私有屬性,這時一定要小心,閉包可能會修改掉父函數的私有屬性。
6、瀏覽器的垃圾回收機制
瀏覽器的垃圾回收機制有兩種機制:引用計數和標記清除
- 引用計數:一個對象不被其他對象引用時會被回收。(存在問題:循環引用時無法回收)
- 標記清除:對所有活動對象進行標記,從根元素開始,周期性標記可被訪問的對象,同時回收不可被訪問的對象。等到清除階段會將沒有標記的對象清除。存在問題:收集垃圾時程序會等待,且回收后的內存空間不連續,于是出現了 標記整理機制,即回收后會整理內存空間,但效率又會降低一些。
6.1、局部變量和全局變量的回收時間點
不再使用的變量也就是生命周期結束的變量,是局部變量,局部變量只在函數的執行過程中存在,當函數運行結束,該變量沒有其他引用時(閉包),那么該變量就會被回收。全局變量的生命周期直至瀏覽器卸載頁面才會結束,也就是說全局變量不會被當成垃圾回收。
(局部變量應該是只有在函數執行時才會被創建然后占用內存,當函數執行結束之后如果沒有閉包引用它的話就會被回收。當函數再次執行時又再次創建然后占用內存)
可以參考:https://blog.csdn.net/zxd10001/article/details/81038533
6.2、JavaScript的內存生命周期
- 分配所需要的內存
- 使用分配到的內存(讀、寫)
- 不需要時將其釋放
垃圾回收機制的原理其實很簡單:確定變量中哪些還在繼續使用的,哪些已經不用的,然后垃圾收集器每隔固定的時間就會清理一下,釋放內存。
局部變量在程序執行過程中,會為局部變量分配相應的空間,然后在函數中使用這些變量,如果函數運行結束了,而且在函數之外沒有再引用這個變量了,局部變量就沒有存在的價值了,因此會被垃圾回收機制回收。在這種情況下,瀏覽器很容易辨別哪些變量該回收,但是并非所有情況下都這么容易。比如說全局變量。在現代瀏覽器中,通常使用標記清除策略來辨別及實現垃圾回收。
- 標記清除
標記清除會給內存中所有的變量都加上標記,然后去掉環境中的變量以及不在環境中但是被環境中變量引用的變量(閉包)的標記。剩下的被標記的就是等待被刪除的變量,原因是環境中的變量已經不會再訪問到這些變量了。最后垃圾回收器會完成內存清理,銷毀那些被標記的值釋放內存空間。
參考:http://www.rzrgm.cn/yunfeifei/p/4019504.html、 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

浙公網安備 33010602011771號