闭包定义
MDN的官方解释:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
一般理解:在嵌套定义的函数中
闭包让你可以在一个子函数中访问到父函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
作用域
javascript的变量作用域分为两种类型
- 全局作用域:直接在script标签内部定义或函数内部未声明直接赋值的变量(就是没写var直接 变量名=值)。这类变量直接作为window对象的属性保存,可以被直接访问或者是被函数内部的语句访问。
- 函数作用域:定义在函数声明内部的变量。一般情况下不能被函数外的语句直接访问,只能被函数内部的语句访问。
作用域链
不同作用域相互链接成一个作用域链。
javascript在执行函数时会查找变量并对变量值进行处理。针对函数内部一句处理num1这个变量的语句,执行中num1的值会首先在函数对象中寻找,函数对象中没有就到函数对象的上一级对象中寻找num1这个变量,上一级对象中没有这个变量时就继续到上一级去寻找。一直寻找到window对象,如果还没有,就会抛出ReferenceError引用错误。
eg1:直接在script标签中定义的函数会先在函数内部寻找,没有再到window对象中(全局作用域)中寻找变量。
eg2:在某个自定义对象中定义的方法,寻找变量时会先在函数内部中寻找,没有就到对象中寻找,再没有就到自定义对象的上一级去寻找,上一级可能是window对象,再寻找不到就抛出错误,也可能是另一个对象,重复寻找这个过程一直持续到找到或抛出错误。
闭包实例
var a=12;
function test (){
var a=13;
function f(){return a;}
return f();
}
console.log(test());
这里输出结果为13。
var a=12;
function test (){
var a=13;
function f(){return a;}
return f;
}
console.log(test()());
这里输出结果也是13。两次test函数的返回值不同,前者返回值直接是值13,后者返回值为f这个函数,函数再执行。这时f是在局部作用域定义的,执行时依然寻找局部作用域中的a返回,这就使我们可以在外部访问函数内部定义的变量。
闭包实现效果
闭包使得我们可以从外部访问函数内部的变量。使得函数内部变量不会在函数执行完毕后无法访问,被垃圾回收机制回收。使用闭包,我们可以实现
- 外部访问函数内部变量。
- 函数内部变量常驻内存。
实际开发中常用闭包实现for循环绑定事件。代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
li{
width: 100px;
height: 100px;
background-color: lightblue;
list-style: none;
float: left;
}
</style>
</head>
<body>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
<script>
var lis=document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++)
{
lis[i].onclick=function(){
console.log(i+1);
}
}
</script>
</body>
</html>
这里实际实现的效果是,点击任意li都会输出10,而不是对应的123······。以下介绍为什么会出现这种情况:
绑定点击事件的函数内部没有定义变量i,变量i是引用外部的for循环中的i。点击事件在for循环执行中被绑定到对应的li上,但是只是指定输出内容为i,点击事件触发时引用i的值,这时js代码已经执行完,for循环自然也已经结束,这时i已经是10,为每个li元素绑定的都是输出10的点击事件。
修正方案:
var lis=document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++)
{
(function f(i){
lis[i].onclick=function(){
console.log(i+1);
}
})(i);
}
这里在for循环里立即执行一个函数,函数参数是i,执行后为函数绑定点击事件输出i。
使用闭包还可以封装对象的私有属性和方法。这里不做赘述,另有博文介绍。
底层细节
函数在调用时,会产生一个执行期上下文(active object)对象。函数拥有的变量都会变成这个对象的属性。每次调用该函数,都会创建一个这样的对象。这个对象在函数调用时创建,在函数执行结束后,如果没有方式可以再访问这个函数对象,这个函数对象就会被当作内存垃圾被清除。当函数内部定义一个嵌套函数并把这个函数对象返回到外部时,这个被返回的嵌套函数对象是在函数内部定义的,可以访问被嵌套函数外部的-函数内部的变量。我们通过返回这个嵌套函数来获得访问函数内部变量的可能性,从而使垃圾回收机制不能把执行期上下文这个对象回收(已经有访问的方式)。
值得注意
当内层函数保留了外层函数中变量的引用时,外层函数的变量就不再是无法被访问的变量,而是可以被内层函数调用并访问的变量,不会被javascript的垃圾回收机制清理掉。这样可能导致内存垃圾持续占用内存,导致内存泄漏。因此,在使用闭包的情况下需要确保内存泄漏的处理。可以在代码最后将变量的引用置为null,使内存垃圾无法被引用,进而被处理。