Javascript闭包“浅”析

回顾

3月时候其实浅显的说过一次闭包的概念。然而随着实践和了解的加深,尤其是最近阅读了一些书籍,有了现在这篇文章。此文谨献给过去那些日子的自己,顺便作为笔记。

闭包是什么

现在认为,闭包是一个作用域。它有以下关键词:

  1. 它(作为闭包的作用域)是函数创建时建立的——这个作用域在执行后可能已经消失(函数执行完毕后作用域被推出),也可能没有(window全局作用域)。
  2. 这个作用域允许函数的自身函数访问和操作函数之外的变量。
  3. 闭包就像一个保护壳,保护着闭包内部变量不被回收。

这些关键词可以帮助我们理解一些概念。

权威指南对闭包的定义: 函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献内称为“闭包”[This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.]

中文部分是中文版的权威指南翻译。但是不得不承认,相对于英文的清晰描述,中文的翻译实在是很误导人。英文的原意是:这种函数对象和其绑定的作用域(作用域是函数内部变量保存的地方)的 结合体,在计算机科学中称之为闭包。

不过原文说这句话时候有很长的一段上下文,这里引用一下。

Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked. In order to implement lexical scoping, the internal state of a JavaScript function object must include not only the code of the function but also a reference to the current scope chain. (Before reading the rest of this section, you may want to review the material on variable scope and the scope chain in §3.10 and §3.10.3.) This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

这段话里面有几个关键的地方:

  • Javacript使用词法作用域(常见的作用域有两种模型,一种是词法作用域Lexical Scope,一种是动态作用域Dynamic Scope)。
  • 词法作用域有一个特性: 函数执行时候使用的作用域,是在函数定义时候决定,而不是执行的时候决定的(而这是动态作用域的特性)
  • 函数和作用域的结合体才是闭包的真正所指

最简单的一个例子

从技术的角度来讲,按照这个内涵,任何的Javascript的函数对象其实都可以称之为闭包。

1
2
3
4
5
var a = 1;
function add(){
a++;
}
add();

实际上,前端从业人员大概每天都在写类似这样的代码,而且写过无数次。然而直到最近,才明白,其实它也算是一个闭包。在这个例子中呢,函数对象和全局作用域绑定在一起了,他们组合一起就可以称之为闭包。

但是这个例子呢,因为绑定了全局作用域,但是全局作用域变量本身存在一个特性,那就是全局作用域里面的变量的生命周期是伴随全局作用域的存在,贯穿整个程序的始终的,如果你不去改变它,它就不会消失。

但是更多的情况下,我们的函数使用的是函数作用域,函数作用域的生命周期是这样的:函数开始执行时创建,函数执行完后局部变量会自动销毁。它伴随函数的执行而生灭,是会消失的。

来个稍微复杂的

1
2
3
4
5
6
7
8
9
var out = "outValue",middle;
function closuer(){
var inner = "innerValue";
middle = function(){
return inner;
}
}
closuer();
console.log(middle());

相对上一个闭包来说,这个例子就可以说明闭包的一些好处。首先这里需要注意到的要点:

  1. closuer执行完毕以后,作用域已经从作用域链中推出了(生命周期走完),也就是说middle()执行时候,这个作用域已经不存在。按照正常的流程这个作用域应该被推出了,但是它没有。
  2. middle可以访问到已经不存在的作用域内的变量–>闭包将它们「保护」起来了。

在这里, 保护这个词我个人认为不是很恰当,它毕竟只是一个比喻。在这个例子中,存在的是两个闭包。

  • 当closuer函数定义的时候,它和全局形成了一个闭包。
  • 当middle函数定义的时候,它和inner所在的作用域形成了一个闭包(当然,它同时是closuer的函数作用域)。

当closuer执行时候,closuer改变了全局作用域的变量使其指向了closuer内部的middle函数。而middle函数在定义的时候就绑定了inner所在的作用域形成了闭包。所以当middle函数执行的时候,它只是访问到了自己作用域的变量。而并非一种特殊的保护。如果非要说有,那么就是middle强行改变了该函数的生命周期,使函数始终被引用了,最终导致与其绑定的作用域无法被释放。

在这里基本上可以给出一个闭包应用的套路了:

  1. 第一步,函数嵌套,函数A包含函数B,此时函数B作为闭包(参考前文,任何Javacript的函数其实都是闭包)会绑定到函数A的函数作用域。
  2. 第二部,函数A最终会讲函数B作为返回值
  3. 设定一个全局变量(生命周期决定它不会被回收)然后讲它赋值为函数A的执行结果。But,理论如果你定一个生命周期有限的变量(只要它的生命周期收起大于A便可),也可以在它的生命周期里面享有这个闭包——但你如何实现它?函数的执行应该是快速而果断的,人为制造一个类似下列的耗时的函数,其实用处不大。
1
2
3
4
5
6
7
8
9
10
11
12
function a(){
function b(){
var b = 1;
return function () {
return b
}
}
var a = b()()
setTimeout(function () {
console.log(a)
},1000)
}

关于闭包一些特性

  • 内部函数的参数是包含在闭包中的,关于这个例子 参见 Javascript闭包作用场景二
  • 闭包作用域内的其他变量(指处于定义时作用域之外的变量),即使是在闭包产生后定义,也可以被闭包访问到。

最后一句话, 几年过去,对闭包的理解不断加深,到今天觉得应该是把握到了核心的地方了。闭包关键点在于2个:

  1. Javascript一切函数即闭包
  2. 闭包是函数对象和作用域的联合体,然而Javascript内不存在函数没有绑定作用域

end