在我學習javascript的事件時,有一個小任務是使用JS來實現 li 列表項在鼠標懸浮時會有背景陰影的動態效果,很天然想到用for 來爲每一個列表項添加onmouseover 和 onmouseout事件來改變和恢復 li 的類名。javascript
以下:html
1 <script type="text/javascript"> 2 var Lis = document.getElementsByTagName("li"); 3 4 function addevent() { 5 for (var i = 0; i < Lis.length; i++) { 6 Lis[i].onmouseover = function() { 7 console.log(i); 8 Lis[i].className = "lihover"; 9 } 10 Lis[i].onmouseout = function() { 11 Lis[i].className = ""; 12 } 13 } 14 } 15 addevent(); 16 // console.log(i); 17 </script>
看起來頗有道理的代碼會什麼不能正常工做?java
先看一下爲何編程
在第8行和11行,經過改變 li 元素的classname來實現鼠標懸浮的動態效果改變,根據我之前學習的語言(C和JAVA)這明顯是不對的,怎麼能在函數內使用外面的局部變量呢,但是瀏覽器爲何沒有報錯,我在7行加了一句console.log(i);看一下瀏覽器
瀏覽器輸出了3 並報錯:closure.html:44 Uncaught TypeError: Cannot set property 'className' of undefined閉包
當這兩個事件發生時,函數會執行,函數會訪問Lis 和 i 這兩個變量,可是注意循環和事件函數不是同時執行的,事件函數發生時i的 已是3了, 而Lis裏並無Lis[3]這個元素。函數式編程
但是這兩個函數爲何能訪問外面的局部變量呢,答案是 閉包 (closure)函數
閉包就是在建立函數時爲其保存一份建立時的外部環境,因此這兩個事件函數可以訪問到i這個變量,雖然訪問的是循環執行完後的i的值。這也已經很神奇了對吧,源自函數式編程的魔法。那麼怎麼讓其訪問的 i 是函數自身被建立時的 i 呢,我再建立一個函數專門用來返回這個事件函數,以下:學習
1 function makeevents(i) { 2 console.log(i); 3 return function() { 4 Lis[i].className = "lihover"; 5 } 6 } 7 8 function addevent() { 9 for (var i = 0; i < Lis.length; i++) { 10 Lis[i].onmouseover = makeevents(i); 11 Lis[i].onmouseout = function() { 12 this.className = ""; 13 } 14 } 15 } 16 addevent();
我只是在mouseover事件用了這個機制,在mouseout使用了this關鍵詞這個等最後在討論。this
在瀏覽器裏運行一下發現效果已經實現了,而且在頁面加載完後控制檯就打印出了0 1 2
makeevent函數的做用正是建立閉包,在每一次循環建立一個,這樣Lis[i]引用的就是正確的 li 元素了,看起來好像很繁瑣的樣子,事實上咱們由於能夠對makeevent傳入參數來改變返回的函數的一些特色。這好像面向對象的工廠模式對吧。事實上咱們徹底能夠用閉包來實現面向對象的封裝。
我在第二個事件函數裏使用了this,它表示了響應這個事件的當前對象,也即表示當前的列表項。而且在必要時還能夠往Lis[i]元素對象里加入其餘東西,以下:
Lis[i].index = i; Lis[i].hehe = "hehe"; Lis[i].onmouseout = function() { console.log(this.hehe); Lis[this.index].className = ""; }
這樣寫也是能夠的。經過對象自己能夠動態的添加屬性這一個JavaScript的特色來完成的。
是的,JavaScript真是一門神奇的語言