在我学习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真是一门神奇的语言