javascript
JavaScript:从此不再怕闭包
閉包就好像從JavaScript中分離出來的一個充滿神秘色彩的未開化世界,只有最勇敢的人才能到達那里。——《你不知道的JavaScript 上卷》
1、起源
js閉包很長一段時間里都是讓我頭疼的一個東西。工作中遇到類似這樣的代碼就很怕:
需求
頁面內三個按鈕,點擊按鈕控制臺輸出按鈕在所有按鈕中的序號,序號從1開始
說明
當然,實際的應用中我們一般不會有這么單純的需求,也不會寫這么刻意的代碼,這里我們為了學習,強行挖個坑,自己再填坑。
上代碼
<html> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>閉包</title> </head> <body>點擊顯示按鈕序號<br><button>click</button><button>click</button><button>click</button><script>var btns = document.querySelectorAll("button");for(var i=0;i<btns.length;i++){btns[i].onclick = function(){console.log(i+1);}}</script> </body> </html> 復制代碼不懂閉包前,我就覺得,這很優雅啊,按鈕在集合btns中的索引+1正好就是滿足需求的。興奮地趕緊自測,咔咔咔連點三下。
結果
當時內心表情大概就像上面這個哥們。但還是在工位上故作鎮定地趕緊百度了下。
修正
百度和修改后的for循環就變成了這個樣子,用一個閉包存一下數組索引i的值傳給閉包內的函數。
for(var i=0;i<btns.length;i++){btns[i].onclick = (function(tmpI){return function(){console.log(tmpI+1);} })(i); } 復制代碼click,click,click。大功告成!
2、原理
工作中滿足了當時的需求也就立馬沉迷代碼擼下一個功能去了。但對閉包的詳細原理知之甚少,相關問題稍微發生點變化,就又可能讓自己云里霧里。近來專門找了幾本書,刻意攻克了下,才算開始了解了閉包。
如上問題的解析
代碼再貼一下:
var btns = document.querySelectorAll("button"); for(var i=0;i<btns.length;i++){btns[i].onclick = function(){console.log(i+1);} } 復制代碼上面代碼不能按需實現功能的問題在于: 1、for循環內只是將匿名函數引用賦給onclick方法,在函數被調用時才會去實時取i的值 2、i為全局變量,在onclick方法觸發時,i的值已被i++的操作變成了4
for(var i=0;i<btns.length;i++){btns[i].onclick = (function(tmpI){return function(){console.log(tmpI+1);} })(i); } 復制代碼上述代碼,onclick方法獲得的匿名函數通過立即執行函數返回,立即執行函數內又通過傳參的方式,將i的值傳入,用tmpI變量保存起來。確保了外部的i++操作不會影響函數內的tmpI的值的變化,就解決了問題。
閉包定義
"閉包(closure)是一個函數在創建時允許自身函數訪問并操作該自身函數之外的變量時所創建的作用域。"——《JavaScript 忍者秘籍》
這是我覺得相對來說比較易懂的對閉包的解釋。關鍵要素有兩個: 1、函數創建時產生。 2、允許自身訪問并操作函數之外的變量。 上述代碼return的匿名函數中訪問了其函數之外的tmpI變量,形成了閉包。
按書中定義,如下我們大家每天都在寫的代碼也屬于閉包。
var outerValue = "外部變量";function outerFunction(){console.log("outerValue",outerValue); } outerFunction(); 復制代碼按定義,outerFunction確實訪問了其外的outerValue值,屬于閉包。但因為函數定義和外部變量都處在全局作用域中,該作用域從未消失過,所以我們也沒覺得這有什么特殊的地方。通過實際的chrome調試發現,chrome也不會標記此類的閉包。以下,我們和chrome保持一致,不再特別說明全局作用域閉包。
再舉個栗子
var outerValue ="外部變量"; var later;function outerFunction(){var innerValue = "內部值";function innerFunction(paramValue){console.log(outerValue,"Inner can see the outerValue.");console.log(innerValue,"Inner can see the innerValue.");console.log(paramValue,"Inner can see the paramValue.");console.log(tooLate,"Inner can see the tooLate.");}later = innerFunction; }console.log(tooLate,"Outer can't see the tooLate."); var tooLate = "outerFunction之后聲明的變量";outerFunction(); later("later參數"); 復制代碼輸出結果如下:
上述例子引用自《JavaScript 忍者秘籍》,筆者做了部分修改。這里innerFunction函數創建時形成了閉包,其訪問了outerFunction中的innerValue。 其余部分的代碼書中是為了說明閉包的三種更有趣的性質。
1、內部函數的參數是包含在閉包中的。(這是顯而易見的)
2、作用域之外的所有變量,即便是函數聲明之后的那些聲明,也都包含在閉包中。(在調用later時可以訪問到tooLate變量)
3、相同的作用域內,尚未聲明的變量不能進行提前引用。(如代碼中先打印的tooLate為undefined,我覺得這也是顯而易見的。)
chrome中對outerFunction閉包的標識如下:
在調用outerFunction,定義innerFunction時,訪問了innerValue,形成了outerFunction閉包。
3、延伸
按我的理解,js有閉包的概念是因為js設計之初沒有塊級作用域,只能通過函數來限制變量的有效范圍。當有了塊級作用域,其實也就不再需要寫閉包。如開頭例子如果用ES6 let改寫,也可實現需求功能。
var btns = document.querySelectorAll("button"); for(let i=0;i<btns.length;i++){btns[i].onclick = function(){console.log(i+1);} } 復制代碼讀者可以試下,這也是滿足需求的。這是因為使用let定義i,ES6做了處理,每次循環的i都有各自的作用域,其值不會互相影響,函數調用時,其仍然保持了原值。
文中內容有錯誤,或讀者對此仍有疑惑的,歡迎大家在評論中留言或者加微信一起探討學習。
主要參考資料
《你不知道的JavaScript 上卷》——[美]Kyle Simpson
《JavaScript 忍者秘籍》——[美]John Resig,Bear Bibeault
轉載于:https://juejin.im/post/5ce12c285188254081056efa
總結
以上是生活随笔為你收集整理的JavaScript:从此不再怕闭包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java集合系列之18 spring b
- 下一篇: 好程序员Web前端分享无法忽视的Java