Skip to content

Instantly share code, notes, and snippets.

@jinwei233
Last active August 29, 2015 14:20
Show Gist options
  • Save jinwei233/e087e78ff74d6bb15c74 to your computer and use it in GitHub Desktop.
Save jinwei233/e087e78ff74d6bb15c74 to your computer and use it in GitHub Desktop.
我对 JavaScript 闭包的理解

在 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
  1. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment