在 JavaScript 中,创建一个 表达式 —— 通常是一个 函数表达式 时,这个函数以及创建函 数时的 环境 共同构成了 闭包 。
举个例子,一个数数的函数:
function Counter(){
var n = 0;
return function(){
n++;
console.log(n);
}
}
var c = Counter();
c(); // => 1
c(); // => 2
// ... 继续调用 输出值会递增
Counter 返回一个数数的函数,每调用一次,其输出的值+1
调用 Counter 函数时,返回值是一个函数,引擎在处理这个返回函数会将这个函数以及 这个函数的执行环境 —— 包含两部分:1) 内部环境,也就是 n=0;2) 外部环境,这里是 全局环境,全局环境里没有其它变量,同时保存起来。这样,这个函数及其执行环境就构 成了一个闭包。
闭包的环境部分存储的是 引用 ,这样导致的结果是,针对这个例子而言,闭包的表达 式部分 —— 也就是函数在进行求值时,改变了闭包的内部环境变量 n ,它在不断递增,这 样就实现了一个递增的计数器的功能。
作为另一个例子,执行环境包含其它变量的:
// 数数
var m = 0;
function Counter(){
var n = 0;
return function(){
n++;
console.log('n=',n);
m++;
console.log('m=',m);
}
}
var c = Counter();
c(); // => n=1 m=1
c(); // => n=2 m=2
跟上面一样,很好理解: return function(){…} 内部对 n 的访问是对函数 Counter 内部作用域的访问,对 m 的访问是对全局作用域的访问。
注意,这里其实隐含了两个概念 作用域 和 作用域回溯 .
比如求 console.log(n) 的值时,先从匿名函数 return function(){} 内部查找 n —— 匿名函数内部是一个 作用域, 没有 n 这个变量,然后再查找匿名函数外部,也就是 Counter 函数内查找,Counter 函数又是一个 作用域 ,找到了变量 n,n 的求值结束;
类似的,对于 m 而言,先从匿名函数 return function(){} 内部查找 m , 没有 m 这个 变量,再从匿名函数 return function(){} 外部查找 ,也就是 Counter 函数内查找, 没有 m 这个变量,那么继续往 Counter 的外部查找,也就是 全局作用域 查找,找到 m, m 的取值结束。
这样逐步由内层作用域向外部作用域查找变量值的过程,叫 作用域回溯 ,每个作用域 有一个 环境 相对应,在 作用域 内求值的过程,就是查找对应的 环境 变量。作 用域回溯和原型链回溯有相似之处,这里不做分析。
为什么最开始要说 「通常是一个函数表达式」,因为在 js 中还有一种表达式, with 表达式也会产生闭包环境,请看下面的例子:
var outer = {a:1};
var inner = {a:2};
var empty = {};
// 1.
with(outer){
with(empty){
console.log(a);
}
}
// => 1
// 2.
with(outer){
with(inner){
console.log(a);
}
}
// => 2
- 2. 是两个嵌套的 with 表达式,第一个表达式在执行内部的 with 表达式时,其所在的
内部环境为 empty ,往外的环境是 outer,a 的求值规则为先从 with 语句内部作用域 查找,也就是 {} 包尾的部分,然后再到环境 empty 中查找,再到 outer 中查找,最后 到全局环境中查找。也就是说这两个嵌套的 with 表达式与其环境构成了一个闭包,因为 它符合两个条件:表达式,以及表达式执行所需要的环境。再来两个 with 表达式的例子, 自行分析理解:
// 3.
var a = 1
with(empty){
console.log(a);
}
// => 1
// 4.
var a = 1
with(empty){
var a = 2
console.log(a);
}
// => 2
所以,在 MDN 中对闭包的解释是这样的:
A closure is a special kind of object that combines two things: a function, and the environment in which that function was created
严格来说不一定是 function ,with 表达式也是可以的.
另外值得一提的是,由于 closure 在 1975 年由 scheme 中最先实现 [1] ,scheme 是 Lisp 的方言,在 Lisp 系的语言中,一段程序就是一个一个的表达式,一个函数也是一个表达 式,而在 JavaScript 中一般不说 表达式 ,而说 语句 ,我这里不严谨的说为表达 式了。
[1] 参见 winter 的考证文章 wintercn/blog#3