闭包(closure)
就是能够读取其他函数内部变量的函数。在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成 定义在一个函数内部的函
。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
如果你还不懂,没关系,我们一步步分析
# 一、变量的作用域
要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
var n=999;
function f1(){
console.log(n)
}
f1(); // 999
而另一方面,在函数外部自然无法读取函数内的局部变量。
function f1(){
var n=999;
}
console.log(n); //报错:Uncaught ReferenceError: n is not defined
这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
function f1(){
n=999;
}
f1();
console.log(n); // 999
# 二、从外部读取局部变量
正常情况下,我们并不能拿到局部作用域的变量。但是,我们可以使用变通的方式:在函数的内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
console.log(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
function f1(){
var n=999;
function f2(){
console.log(n)
}
return f2;
}
var result=f1();
result(); // 999
这就是闭包
让我们来看看这个函数做了什么:
- 创建了一个函数
f1
- 函数里面创建了一个变量
n
与函数f2
- 返回函数
f2
现在,我们就对闭包有了一个基本的概念:定义在一个函数内部的函数
# 三、闭包的典型使用
我们再来看一个例子
for(var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
我们预想的结果是一秒后会循环输出1 2 3 4 5
但是运行上面的代码会输出 5
个 6
,很明显,我们得到的结果却是 i
在循环之后的最终值。那么,为什么会是这样呢?
其实,由于作用域的工作方式,我们在定时器函数中访问到的i是共享到全局作用域的上的,它只有一个,就是最终循环结束的值
想让这个循环显示我们想要的结果也很简单,只需要这样:
for(var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j)
}, 1000)
})(i)
}
使用一个立即执行函数将定时器函数包裹起来,在这个函数中定义一个变量 j
,然后将 i
当做值传递进去。这样,在每次迭代的时候,变量 j
都会拥有 i
的一个拷贝,自然得到了我们想要的结果
同样的,这也是一个闭包的典型应用
# 四、闭包的用途
闭包有很多用途:
(1)读取函数内部的变量
(2)让这些变量始终保存在内存中
怎么来理解这句话呢?请看下面的代码。
function f1() {
var n = 999;
nAdd = function () {
n += 1
}
function f2() {
console.log(n)
}
return f2;
}
var result = f1();
result(); // 999
nAdd(); //调用f1函数中的nAdd方法
result(); // 1000
在这段代码中,result
实际上就是闭包 f2
函数。它一共运行了两次,第一次的值是 999
,第二次的值是 1000
。这证明了,函数 f1
中的局部变量 n
一直保存在内存中,并没有在 f1
调用后被自动清除
(3)保护函数内的变量安全
现在我们要实现一个功能:统计一个函数调用的次数,需求很简单,直接开写
var count = 0;
function fn() {
count++;
console.log(count)
}
fn(); //1
fn(); //2
fn(); //3
但是由于count是可以被外界随意访问的,这时突然有人恶意捣乱
var count = 0;
function fn() {
count++;
console.log(count)
}
fn(); //1
fn(); //2
count = 0;
fn(); //1
按理说应该是调用3次,可是输出的次数却是1次,这样统计的次数就不正确了,也就是说这个数据就不安全了。接下来我们用闭包改写
function outer() {
var count = 0;
function inner() {
count++;
console.log(count)
}
return inner;
}
var result = outer();
result();//1
result(); //2
//count = 0 错误,无法访问到count,因为此时count不再是全局变量
result(); //3
这样的话,闭包把count数据给保护起来了,外界就无法访问和修改count变量,也就保证了变量的安全性
(4)模块化代码(数据隐藏和封装)
对js代码进行模块化封装和构建,不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分
var mytools = (function(){ //aaa是一个模块,私有变量是a,私有函数是bbb和ccc
var a = 1;
function bbb(){
a++;
alert(a);
}
function ccc(){
a++;
alert(a);
}
return {
b:bbb,
c:ccc
}
})() //函数表达式自执行,结果是return后面的对象
mytools.b(); //2
mytools.c(); //3
# 五、闭包引发的问题
闭包会使函数中的变量始终保存在内存中,不能被 JavaScript`的垃圾回收清理,很容易造成内存消耗过大,影响程序性能,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
解决方法是,在退出函数之前,将不使用的局部变量全部删除。