闭包

网上有很多关于闭包的介绍,但是对于新手还是没有那么友好。我尝试分享一下自己对闭包的理解,希望对大家理解闭包有一点点帮助。当然,如果有错误也希望大家能够及时指出。

1、概念

首先我们先看一下《you don’t know js》中对于闭包概念的介绍:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

词法作用域

这里有一个关键词出现了两次,叫做词法作用域。说明闭包和词法作用域有很大的关系。在理解闭包之前,肯定是需要对词法作用域有一个比较清楚的了解的。
对于作用域,我想学过编程的肯定都不会陌生的吧,它是管理引擎如何寻找变量的一个规则。作用域分为两种模型,一种叫词法作用域,也就是JavaScript采用的,还有一种叫动态作用域。
JavaScript具有基于函数的作用域(凡事无绝对,with、catch是块作用域,ES6也引入了块作用域这个概念),什么意思呢,就是你可以在函数内访问函数内的变量,如果没有找到还可以往上层寻找直到全局作用域。但是如果在函数外是不能访问函数内的变量的,简单来说就是往上不往下。
举个例子:

1
2
3
4
5
6
7
8
9
function foo()
{
var a = 1;
console.log(a);
a+=1;
}
foo(); //1
foo(); //1
console.log(a); //ReferenceError: a is not defined

函数foo里面定义了一个变量a,但是你在函数外面是不能访问到a的。同时,每一次执行foo()方法后foo()的整个内部作用域都被销毁了。


1
2
3
4
5
6
var a = 1;
function foo()
{
console.log(a); //1
}
foo();

当执行foo()的时候由于foo里面并没有a这个变量所以就会往上一级寻找,于是在全局作用域中找到了a。


2、结构

我们看一个闭包的简单例子:

1
2
3
4
5
6
7
8
9
10
11
function foo()
{
var a =1;
function bar()
{
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //1

很神奇,我们居然在foo()的外面访问到了foo()里面定义的变量a并且把它输出了。这就是闭包!由于浏览器gc存在,会释放不再使用的空间,看上去foo()执行完后不会再继续使用了,所以应该对foo()的作用域进行回收。但是由于存在闭包,所以并没有回收foo()的空间,这就是闭包的作用。
我们来解释一下代码,foo()执行后返回一个bar函数,baz其实是对bar函数的引用,baz()相当于执行了bar(),bar()在定义它的词法作用域外执行了。


当然,不只是return一个函数才会创建一个闭包,我们还可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
var fn;
function foo()
{
var a = 1;
function bar()
{
console.log(a);
}
fn = bar;
}
foo();
fn(); //1

或者这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo()
{
var a = 1;
function bar()
{
console.log(a);
}
baz(bar);
}
function baz(fn)
{
fn();
}
foo(); //1

总而言之,言而总之,无论通过什么办法将内部函数传递到定义函数时所在的词法作用域以外,它都会持有对原作用域的引用,在任何地方执行该函数(包括定义的函数内)都会产生闭包。


3、用途

用途也可以称作好处,我觉得有以下两个好处:

3.1、模拟块作用域(ES6之前是没有块作用域的概念的)

首先,我们先看一个很经典的代码:

1
2
3
4
5
6
for(var i =0;i<5;i++)
{
setTimeout(function(){ //setTimeout()是在全局作用域中执行的--<<JavaScript高级程序设计>>
console.log(i);
},i*1000);
}

这段代码的本意是每秒钟分别输出0,1,2,3,4,可以结果却是每秒都输出一个5。
问题出在哪里呢?
原来,这几个延迟调用的函数都是在全局作用域下的,它们共享一个i,所以都会输出相同的值。

那这个问题该怎么解决呢?通过立即执行函数表达式(IIFE)创建闭包作用域。

1
2
3
4
5
6
7
8
for(var i =0;i<5;i++)
{
(function(j){
setTimeout(function(){ //setTimeout()是在全局作用域中执行的--<<JavaScript高级程序设计>>
console.log(j);
},j*1000);
})(i);
}

这样就会每秒依次输出0,1,2,3,4了。
当然,ES6有一个叫 let 的关键字,也可以通过它创建块作用域。

1
2
3
4
5
6
for(let i=0;i<5;i++)
{
setTimeout(function(){
console.log(i);
},i*1000);
}

3.2、用于在对象中创建私有变量。

如果不想变量暴露太多,把变量放到函数里,想暴露谁就用函数在内部return谁。

还是看代码比较清楚:

1
2
3
4
5
6
7
8
9
10
11
12
function foo()
{
var name = "sf";
var age = 25;
function getName()
{
console.log("name:"+name);
}
return getName;
}
var obj1 = foo();
obj1(); //name:sf

这样我想暴露name就暴露name,想暴露age就暴露age,不想暴露的你就看不到。


模块化

我们把上面的代码改造一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function foo()
{
var name = "sf";
var age = 25;
function getName()
{
console.log("name:"+name);
}
function getAge()
{
console.log("age:"+age);
}
return { //返回一个对象,将函数foo的方法传递给该对象的两个属性方法
Name:getName,
Age:getAge
};
}
var obj1 = foo();
obj1,Name(); //name:sf
obj1.Age(); //age:25

这就是一个小小的模块,我们也可以对返回的对象命名,作为公共的API进行调用。