众所周知,JS有全局(Global)变量和局部(Local)变量,其实还有一种,就是闭包(Closure)变量,在Google浏览器调试时会发现它(图1)。那么这个闭包变量是什么,又是如何产生的,又发挥怎样的作用呢?
图1
闭包在实际中运用十分广泛,最重要的是它为应用内存中存储变量的引用提供了一套简便优化的方案。
我们可以通过定义对象,属性,赋值的方式来存储对象信息:
//定义 var Person = function(){ Person.prototype.setName = function(name){ this.name = name; } } //实例化,赋值 var p1 = new Person(); p1.setName("a")
但是很多时候我们并无这样的必要,只是希望某些变量引用不同的信息并存储起来,如:
在这个日历渲染时,我们需要2016年9月份中的每个td元素都指向一个对应的日期,如td(21)指向2016-09-21,那么这种需要一般都是用闭包来实现的。那是如何实现的呢?看完本文相信你会知道答案。
什么是闭包?
JS没有块级作用域,有函数作用域,那么我们如何读取一个函数中的变量呢?答案就是内嵌一个函数,在这个内嵌函数中引用外部函数的变量。如:
例2
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
这样执行函数c我们就可以访问函数a的变量了。可以说,函数b就是一个闭包。
关于闭包,官方的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
在文章javascript深入理解js闭包中对上述示例做出了这样的解释:
让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次那次调用的是各自独立的。
简单地说,对于函数内的局部变量来说,当该函数被调用完毕,如果局部变量没有被引用则就会被GC回收。在实际的情况中,函数内局部变量的值很可能发生变化,那么要储存这些变化的值就可以再嵌套一层函数,并在内嵌函数中引用局部变量,这样它就不会被GC回收,这就是闭包。下面以一个关于闭包的经典案例来说明:
HTML代码:
<form name="form1" onsubmit="checkSelecttion();return false;"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="radio" name="radioGroup"> <input type="submit" value="sub"> </form>
现在的要求是使用原生的JavaScript完成checkSelecttion()函数的内容,实现弹出一个对话框提示当前选中的是第几个单选框。
checkSelecttion函数后的return false;必不可少,否则的话就无法在checkSelecttion函数中为radio添加事件,因为当sub按钮提交后就会执行表单的submit事件,return false;是取消执行默认的事件。
最直接的想法肯定是下面的,但这种写法并不会满足要求。
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { //no. 弹出i的值都是最后一个 radioGroups[i].onclick = function () { alert(i); } } }
变量i是函数checkSelecttion中的局部变量,onclick函数引用了该变量,i就是一个闭包变量(Closure variable),它会被存储起来,但问题是i由于循环会指向不同的值,循环完毕,i会指向最后一个值:radioGroups.length-1,那这样的话,每次弹出i的值就是radioGroups.length-1。
解决该问题就是运用闭包,把i变成onclick函数的局部变量,并再嵌套一层函数引用它,这样就可以存储变化的i的值了:
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { //按参数传递 radioGroups[i].onclick = function (i) {//形参【必须】 return function () { alert(i); } }(i);//实参【必须】 } }
因为JS的参数传递是按值传递,相当于copy一份参数值(或者说相当于在onclick函数内将i定义成了该函数的局部变量),所以当i是0时,由于onclick函数的内嵌函数有引用i,所以它会被存储起来,当i是1...同理都会被存储起来。这样就实现了用闭包存储函数内变化的局部变量值的目的。
当然,下面的2种方式同理都可以达到相同的效果:
1.下面的这种方式与上面的区别是在定义自执行函数没有传递参数,那么在onclick函数内定义局部变量i_接收i,i和i_都是闭包变量,它们都会被存储起来,但是被存储i的值只有一个,就是radioGroups.length-1,而i_的值却被存储成不同的值。
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { radioGroups[i].onclick = function () { var i_ = i;//定义局部变量 return function () {//嵌套一层函数形成闭包存储局部变量不同的值 alert(i_); } }();//自执行函数 } }
2.下面的方式只是自执行函数的另一种写法:
function checkSelecttion(){ var radioGroups = document.getElementsByName("radioGroup"); for( var i = 0; i < radioGroups.length; i++ ) { radioGroups[i].onclick = (function (i) { return function () { alert(i+1); } })(i);//自执行函数 (function(param){})(param) } }
1. (function(param){})(param); 与 function(param){}(param)效果是一样的,均表示自执行函数。
2.如何使用闭包存储函数中变化的局部变量?——在该函数中再嵌套一层函数,并引用它。
深入理解闭包
要想深入理解闭包,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。现在,我们以例1为例阐述这几个概念:
- 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
- 当执行函数a的时候,a会进入相应的执行环境(excution context)。
- 在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
- 然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
- 下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
- 最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是:
- 先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
- 如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
- 如果整个作用域链上都无法找到,则返回undefined。
闭包的应用场景
1.在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
2.通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)。
Javascript的垃圾回收机制
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
闭包中的this
在下面的代码中,为什么结果是“The Window”呢?
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { return function() { return this.name; }; } }; alert(object.getNameFunc()()); //The Window
在JS中,内嵌函数归属于window,也就是说上面代码中的匿名函数中的this指的是window,故而打印的结果就是全局变量"The Window"了。
那么如何访问局部变量"My Object"呢?很简单,我们在匿名函数外创建变量that,指向object,这样打印的结果就是"My Object"了。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function() { var that = this; return function() { return that.name; }; } }; alert(object.getNameFunc()());//My Object
实践:模拟日历渲染
如本文的篇首所说,通常用JS写的日历插件在渲染每个日期(一般是td元素)时会用闭包来实现让当前的td元素引用对应的日期数据,如:
假设这就是日历中2016年的9月份的1~9日,现在就需要将这些数字与具体的日期的对应数据在应用中存储起来,如6对应2016年9月6日:
代码如下:
//为元素绑定事件 var addEvent=function(env,fn,obj){ obj.addEventListener(env,fn,false); }; var render = function(h){ var that = this; addEvent('click',function(){ alert(h); },that); } var dc = function(a){ return document.createElement(a); } var table = dc('table'); var tr = dc('tr'); table.appendChild(tr); for(var i = 1;i < 10;i++){ var d = new Date() d.setDate(i); var td = dc('td'); td.innerHTML = i; render.apply(td,[d]); tr.appendChild(td); } table.setAttribute("border","1px"); table.setAttribute("bordercolor","red"); table.setAttribute("cellspacing","0px"); document.body.appendChild(table);
这里我的脚本的位置在<body>前面,所以这个脚本前加个<div id="bixu"></div>,否则执行document.body.appendChild(table);因为document的body还未解析会报错,最好是把该脚本写到<body>标签后面。
由于在内嵌函数这有引用render函数的局部变量h,所以每次调用render函数时h的引用数据均被存储下来。
var render = function(d){ var that = this; var h= d; addEvent('click',function(){ alert(h); },that); }
其他实例
以下实例来自:前端开发必须知道的JS之闭包及应用
1.保存会不断发生变化的数组值
这个是我在用js模拟排序算法过程遇到的问题。我要输出每一次插入排序后的数组,如果在循环中写成
setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);
会发现每次输出的都是最终排好序的数组,因为arr数组不会为你保留每次排序的状态值。为了保存会不断发生变化的数组值,我们用外面包裹一层函数来实现闭包,用闭包存储这个动态数据。下面用了2种方式实现闭包,一种是用参数存储数组的值,一种是用临时变量存储,后者必须要深拷贝。所有要通过闭包存储非持久型变量,均可以用临时变量或参数两种方式实现。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript">< !-- var arr = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0]; var $ = function(id) { return document.getElementById(id); } var Sort = { Insert: function() { for (var i = 1; i < arr.length; i++) { for (var j = 0; j < i; j++) { if (arr[i] < arr[j]) { arr[i] = [arr[j], arr[j] = arr[i]][0]; } } setTimeout((function() { var m = []; for (var j = 0; j < arr.length; j++) { m[j] = arr[j]; } return function() { $("proc").innerHTML += m + "<br>"; } })(), i * 500); //or 写成下面这样也可以 /* setTimeout((function(m) { return function() { $("proc").innerHTML += m + "<br>"; } })(arr.join(",")), i * 500); */ } return arr; } } // --> </script> </head> <body> <div>var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div> <div> <input type="button" value="插入排序" onclick="Sort.Insert();" /></div>Proc: <div id="proc"></div></body> </html>
2.缓存的应用
下面的code是缓存的应用,catchNameArr。在匿名函数的调用对象中保存catch的值,返回的对象由于被CachedBox变量引用导致匿名函数的调用对象不会被回收,从而保持了catch的值。可以通过CachedBox.getCatch("regionId");来操作,若找不到regionId则从后台取,catchNameArr 主要是为了防止缓存过大。
< script type = "text/javascript" > var CachedBox = (function() { var cache = {}, catchNameArr = [], catchMax = 10000; return { getCatch: function(name) { if (name in cache) { return cache[name]; } var value = GetDataFromBackend(); cache[name] = value; catchNameArr.push(name); this.clearOldCatch(); return value; }, clearOldCatch: function() { if (catchNameArr.length > catchMax) { delete cache[catchNameArr.shift()]; } } }; })(); < /script>
同理,也可以用这种思想实现自增长的ID。
< script type = "text/javascript" > var GetId = (function() { var id = 0; return function() { return id++; } })(); var newId1 = GetId(); var newId2 = GetId(); < /script>
3.暂停执行功能
这个是无忧上月MM的例子(点击这里查看原帖),用闭包实现程序的暂停执行功能,还蛮创意的。
< input type = "button"value = "继续"onclick = 'st();' / > <script type = "text/javascript" > <!-- var st = (function() { alert(1); alert(2); return function() { alert(3); alert(4); } })(); // --></script>
把这个作用延伸下,我想到了用他来实现window.confirm。
<html xmlns="http://www.w3.org/1999/xhtml"> <head> </head> <body> < !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" > <title> </title> <script type="text/javascript "> var $ = function(id) { return "string " == typeof id ? document.getElementById(id) : id; } var doConfirm = function(divId) { $(divId).style.display = ""; function closeDiv() { $(divId).style.display = "none "; } return function(isOk) { if (isOk) { alert("Do deleting..."); } closeDiv(); } } </script> <style type="text / css "> body { font-family: Arial; font-size: 13px; background-color: #FFFFFF;} #confirmDiv { 200px; height: 100px; border: dashed 1px black;position: absolute; left: 200px; top: 150px; } </style> <div> <input name="btn2 " type="button " value="删除" onclick="doConfirm('confirmDiv'); " /> <div id="confirmDiv " style="display: none; "> <div style="position: absolute; left: 50px; top: 15px;"> <p> 你确定要删除吗? </p> <input type="button " value="确定" onclick="doConfirm('confirmDiv')(true); " /> <input type="button " value="取消" onclick="doConfirm('confirmDiv')(false); " /> </div> </div> </div> </body> </html>
小结
在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。这就是闭包的作用。也就说遇到需要存储动态变化的数据或将被回收的数据时,我们可以通过外面再包裹一层函数形成闭包来解决。