学习笔记---Javascript闭包
我们知道计算机总是按顺序从前往后顺次执行代码, 但是我们总会有这么一种需求: 对于某段代码我们并不希望它现在去执行, 而是在将来某个时间或者隔多长时间才去执行, 在C#中委托和事件提供了类似的处理及其, 那么在Javascript的世界中是不是也有类似的机制? 答案是肯定的, 那就是闭包(closure).
1. 闭包到底是什么呢? 通俗的讲: Javascript中的所有的function都构成一个闭包, 通常我们使用嵌套函数构成的闭包, 即内层函数(inner)被外层函数(outer)的变量所引用时, 就构成了一个闭包. 也就是说, 在嵌套函数中, 最外层的函数有1个成员, 该成员的值为内层函数的引用, 这样便构成了一个闭包.
2. 先看下闭包的两种形式:
//第一种形式:
function outer(){
var i = 7;
function inner(){
alert(i);
}
inner(); //可以用setInteval(inner,5000);实现每5秒执行一次inner函数
}
//第二种形式:
function outer(){
var i = 7;
function inner(){
alert(i);
}
return inner(); //函数是一种特殊的对象, 这里直接返回函数的引用
}
var f = outer(); //使用f接受返回的内层函数的引用
f(); //调用内层函数
在第一种情况时: outer函数定义局部变量i, 函数inner(), 之后又调用了inner函数. 注意看inner()函数, inner函数中 并没有定义变量i, 这个i是来自于outer函数的局部变量. 当在inner函数被引用的时候, 由于内层的inner函数需要使用变量i, 因此这个运行时定义的变量i不能被释放, 从而在inner()函数执行时就可以取得这个变量的值.
当第二种情况时: 执行outer()函数之后, 将返回inner()函数的引用, 由于外部变量f引用了内部函数inner(), 由于inner()函数同样需要使用变量i, 这个局部变量i也将被外部变量f所引用, 因此变量i依然不能被释放. 与第一种形式的区别是: 我们可以再outer()函数执行之后访问到变量i的值.
如果我们将alert(i); 改成 alert(++i); 那么在函数的外部, 我们执行f(); 此时变量i并没有释放, 结果应该是8, 如果再调用一次f(); 则结果变成9。但是: 如果我们再次调用outer();函数后将会创建一个新的局部变量, 之后再调用f(); 结果则变成了8. 也就是说只要外层函数没有再次创建新变量, 原来的变量i将一直有效.
3. 闭包的引用. 我们主要在一下四种情况下使用闭包:
setTimeout() //当计时器(Countdown timer)结束时, 将执行函数
setInterval() //每间隔多少时间后, 将执行函数
Ajax callback() //在Ajax技术中, 异步处理返回的结果时调用处理结果的函数
event handler() //在Javascrip的事件处理中使用, 调用事件处理器函数
注意: 在Javascript中, 函数是一个独立的对象, 当我们在某类函数的原型上注册一个方法时, 实际上将仅仅记录下函数的引用, 所以我们在通过该类对象调用方法时, 得到也只是该方法的引用, 并不会记录下这个类对象. 如下的实例代码:
//javascript – closure
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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>
</head>
<body>
<img id="movPic" src="pic.gif" alt="movepp" style="margin: 0px; position: absolute;" />
<div>
<script type="text/javascript">
function outer(mvElemId, startX, startY, endX, endY, offset) { //mvElemn表示要移动的对象, start表示开始位置, end表示结束位置, dec表示偏移量
var pic = document.getElementById(mvElemId);
function inner() {
//alert(startX + ","+ startY+","+endX+","+endY);
startX += offset;
startY += offset;
//alert(startX + "," + startY + "," + endX + "," + endY);
if (startX <= endX || startY <= endY) {
if (startX > endX) {
startX = endX;
}
if (startY > endY) {
startY = endY;
}
pic.style.top = startY + "px";
pic.style.left = startX + "px"
}
else {
clearInterval("tick");
}
}
var tick = setInterval(inner, 100); //这里传的是个引用, 不能写inner(), 否则表示只调用一次
}
outer("movPic", 100, 200, 500, 700, 5);
</script>
<script type="text/javascript">
//以上代码的另外一种写法
function outer(mvElemId, startX, startY, endX, endY, offset) { //mvElemn表示要移动的对象, start表示开始位置, end表示结束位置, dec表示偏移量
var pic = document.getElementById(mvElemId);
var tick = setInterval( function() {
startX += offset;
startY += offset;
if (startX <= endX || startY <= endY) {
if (startX > endX) {
startX = endX;
}
if (startY > endY) {
startY = endY;
}
pic.style.top = startY + "px";
pic.style.left = startX + "px"
}
else {
clearInterval("tick");
}
}, 100);
}
outer("movPic", 100, 200, 500, 700, 5);
</script>
</div>
</body>
</html>
//示例2:
<script type="text/javascript"> function Person(name) { this.name = name; } Person.prototype.showName = function() { alert(this.name); } //创建对象后立即执行 var mike = new Person("mike"); mike.showName(); //显示mike //5秒后执行 window.name = "window"; //这里提那家window对象的name属性 setTimeout(mike.showName, 5000); //返回空串, 这里实际上返回的是window的name属性的值 //我们可以通过闭包来解决这个问题 function timeOutShowName(timeout, object, method, args) { function tempFunc() { var tempMethod = object[method]; tempMethod.apply(object, args); //this的第二种用法, 通过apply传递this对象, object代表this, 而args为函数的参数(数组) } setTimeout(tempFunc, timeout); } timeOutShowName(5000, mike, "showName", []); //通过call传递this, 最后可以不加参数
</script>
4. 闭包的处理过程:
在闭包的处理过程中, 涉及到4个概念: 函数的执行环境(execution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain).
函数的执行环境: 即包含与该函数执行过程相关的所有内容的容器. 也就是说, 函数执行需要的所有内容都包含在这个执行环境中. 针对以上4个概念, 函数的执行环境包括活动对象和作用域.
活动对象: 是一个拥有属性的对象, 它不具有原型原型且不能通过Javascript代码直接访问. 该对象包含函数的参数(实参和形参)、成员及内部变量等信息. 注意该对象是执行函数时才创建的对象.
作用域: 作用域实际上是活动对象的一个属性, 其值就是函数的作用域链的引用, 用于指出活动对象中变量的查找顺序(类似于原型链).
作用域链: 用于在不同函数的执行环境(如: 嵌套函数)之间提供联系的手段. 作用域链会随着函数的定义和执行动态发生改变.
函数outer从定义到执行的具体过程:
a. 当定义函数outer时, javascript解释器会将函数outer的作用域链设置为定义outer时的”环境”, 若outer是一个全局的函数, 则作用域链中只有window对象.
b. 当执行outer时, 将会创建执行环境.
c. 在创建执行环境的过程中, 会首先为outer添加一个scope(作用域)属性, 此时该值为定义outer时的作用域链. 即outer.scope = outer;
d. 在创建完执行环境后, 执行环境将先创建一个活动对象(call object). 创建完活动对象后, 将把活动对象添加到作用域的最顶端(可以将作用域链想象成一个堆栈). 此时, outer的作用域链发生了改变, 在outer中将包含outer的活动对象和window对象.
e. 接下来, 会在活动对象上创建一个arguments属性, 他保存着调用outer时所传递的参数.
f. 之后, 把outer函数的形参和内部函数inner()的引用也添加到outer的活动对象中, 至此outer函数的定义才完全结束(注意javascript是动态的语言, outer函数中的对象随着代码的执行动态的增加或修改).
g. 当outer函数的定义完全结束后, 将会经过一个从第3步开始的过程, 而从完成inner函数的定义.
至此, outer函数从定义到执行的步骤就完成了.
函数outer执行时, 将返回inner函数的引用给变量f, 因为inner函数的作用域链包含了outer函数的活动对象的引用, 因此inner函数可以访问outer函数定义的所有变量和函数. 所以, 变量f引用inner函数, inner函数又以来与outer函数, 因此outer函数不会被GC.
执行inner函数时, inner的作用域链包含了3个对象: inner的活动对象、outer的活动对象以及window对象. 见下图:
当函数inner访问一个变量时的搜索顺序是:
a. innner函数先搜索自身, 如果存在则直接返回. 如果不存在, 在继续往后查找.
b. 如果inner函数自身存在prototype(原型)对象, 则查找完自身先查找原型对象.
c. 如果整个作用域链上都查找不到, 则返回undefined
最后, 注意函数的作用域(不是作用域链)在定义函数的时候就确定了, 而不是在函数执行的时候.