1.简单的例子
首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码
<div id="divTest"> <span>0</span> <span>1</span> <span>2</span> <span>3</span> </div>
$(document).ready(function() { var spans = $("#divTest span"); for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } } });
很简单的功能可是却偏偏出错了,每次alert出的值都是4,
每个span的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,
直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击span的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。
简单的修改就好使了(暂且先不谈es6 let)
var spans2 = $("#divTest2 span"); $(document).ready(function() { for (var i = 0; i < spans2.length; i++) { (function(num) { spans2[i].onclick = function() { alert(num); } })(i); } });
创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,函数立即执行,
将i作为实参传给了行参num,而行参就相当于在函数里定义了变量num保存每次i的值,num保存每次i的值。
2.内部函数
让我们从一些基础的知识谈起,首先了解一下内部函数。内部函数就是定义在另一个函数中的函数。例如:
function outerFn () { function innerFn () {}
innerFn(); //成功
}
innerFn(); //报错,innerFn is not defined
innerFn就是一个被包在outerFn作用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。
2.1伟大的逃脱
JavaScript允许开发人员像传递任何类型的数据一样传递函数。也就是说,JavaScript中的内部函数能够逃脱定义他们的外部函数。
例如可以将内部函数指定给一个全局变量:
var globalVar; function outerFn() { console.log('out'); function innerFn() { console.log('inner') } globalVar = innerFn; } outerFn(); //out globalVar(); //inner
调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。
也可以通过在父函数的返回值来获得内部函数引用
function outerFn() { console.log('out') function innerFn() { console.log('inner') } return innerFn; } var fnRef = outerFn(); //out fnRef(); //inner
从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。
这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间
闭包:可以访问外部函数作用域中变量的函数
被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收
创建闭包的常见方式就是在一个函数内部创建另一个函数,就是我们上面说的内部函数
1.2变量的作用域
内部函数也可以有自己的变量,这些变量都被限制在内部函数的作用域中:
每当通过引用或其它方式调用这个内部函数时,就会创建一个新的innerVar变量,然后加1
function outerFn() { console('out') function innerFn() { var innerVar = 0; innerVar++; console.log("inner "+innerVar); } return innerFn; } var fnRef = outerFn(); //out fnRef(); //inner 1 fnRef(); //inner 1 var fnRef2 = outerFn(); //out fnRef2(); //inner 1 fnRef2(); //inner1
内部函数也可以像其他函数一样引用全局变量:
内部函数都会持续地递增这个全局变量的值
var globalVar = 0; function outerFn() { console.log('out'); function innerFn() { globalVar++; console.log('inner '+globalVar); } return innerFn; } var fnRef = outerFn(); //out fnRef(); //inner 1 fnRef(); //inner 2 var fnRef2 = outerFn(); //out fnRef2(); //inner 3 fnRef2(); //inner 4
内部函数会引用到父函数的作用域的变量
function outerFn() {
var outVar=0; console.log('out'); function innerFn() { outVar++; console.log('inner '+outVar); } return innerFn; } var fnRef = outerFn(); //out fnRef(); //inner 1 fnRef(); //inner 2 var fnRef2 = outerFn(); //out fnRef2(); //inner 1 fnRef2(); //inner 2
当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。
3.闭包之间的交互
当存在多个内部函数时,很可能出现意料之外的闭包。
我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数
function outerFn() { var outerVar = 0; console.log('out'); function innerFn1() { outerVar++; console.log('inner '+outVar); } function innerFn2() { outerVar += 2; console.log('inner2 '+outVar) } return { "fn1": innerFn1, "fn2": innerFn2 }; } var fnRef = outerFn(); //out fnRef.fn1(); //inner 1 fnRef.fn2(); //inner2 3 fnRef.fn1(); //inner 4 var fnRef2 = outerFn(); //out fnRef2.fn1(); //inner 1 fnRef2.fn2(); //inner2 3 fnRef2.fn1(); //inner 4
innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,就位innerFn2设置了outerVar的新的起点值。
对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,
而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。
优点:
- 可以让一个变量常驻内存 (如果用的多了就成了缺点
- 避免全局变量的污染
- 私有化变量
缺点
- 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
- 引起内存泄露 (解决方法:赋null,
将闭包引用的外部函数中活动对象清除)