总结:在函数 A(如 foo)中存在某个函数 B(如 bar,且必须是在 A 中定义的),且 B 内至少引用了 A 中的一个“变量”,那么函数 A 就是一个闭包。
请注意,与函数 B 的调用方式没关系。无论 B 是在 foo 内部被调用,还是作为返回值返回,然后在别处调用。
再看一个例子:
function foo() {
var b = () => {
// 由于 b 是箭头函数,内部没有 arguments 对象,
// 所以这个 arguments 对象是 foo 中变量对象的一员,
// 因此 foo 也是一个闭包。
console.log(arguments)
}
return b
}
var f = foo('foo')
f() // { 0: 'foo', length: 1 }
上述这个示例,是为了提醒 B 对 A 中的某个“变量”(指变量、函数、arguments、形参等)的引用,不仅仅是通过 var、function、let、const、class 等关键字显式声明的,还可以是 arguments 对象、形参。换句话说,就是 AO 中的所有变量。
再看,下面示例中 foo 是闭包吗?
function foo(fn) {
var a = 'local'
fn()
}
function bar() {
console.log(a)
}
var a = 'global'
foo(bar) // "global"
答案是 NO。前面总结过一个函数要成为闭包,该函数(foo)内部必须存在另外一个函数(fn),且 fn 内需要 foo 中的某个变量。那不正好引用了 foo 中的变量 a 吗?显然,这理解是错误的。
根据词法作用域可知,函数 bar 的作用域链 [[scope]] 在声明时就已确定且不可变,只含 GlobalContext.VO,因此当查找自由变量 a 时,当 bar 的 AO 内查不到,下一步是前往全局对象下查找,于是就找到了 a 其值为 "global"。所以 fn 内部对 foo 构成不了引用,因此 foo 就不是闭包。
若到这里,对闭包还是懵懵懂懂的,这块引用的内容,请跳过。
突然间,我好像明白了为什么函数内部缺省声明关键字的变量(如 a = 1),在执行时才将其视为全局变量。
继上一篇文章 JavaScript 脚本编译与执行过程简述,再来介绍一下 JavaScript 中神奇的“闭包”(Closure)。
JavaScript 语言是采用了词法作用域。一般情况下,函数、变量的作用域在编写的时候已经确定且不可改变的。除了
eval
、with
之外,它们会在运行的时候“修改”词法作用域,但实际项目中,几乎很少用到它们,欺骗词法作用域会有性能问题,我们可以忽略。还有,千万别把
this
跟作用域混淆在一起,this
与函数调用有关,可以说是“动态”的。而作用域是静态的,跟函数怎样调用没关系。词法作用域也被叫做“静态作用域”。若对词法作用域、执行上下文、变量对象、作用域链等内容不熟悉的话,建议先学习相关知识。到时回来再看闭包的时候,就非常容易理解了。
概念
无论网上文章,还是各类书籍,对闭包的定义都不尽相同。列举几个:
讲实话,我也不知道以上哪个说法更贴切、更符合。当了解作用域链之后,就很容易理解闭包了。
上面提到了自由变量一词,
在 ECMAScript 中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
就我个人认为,闭包不是一个函数,它是一种机制,用于访问自由变量。闭包不是 JavaScript 中专有术语,在上世纪很早就被提出来了,在其他语言(如 Ruby 语言)中,闭包可以是一个过程对象,一个 Lambda 表达式或者是代码块。
Chrome 眼中的闭包
其实上面概念可能很多人都不理解,但问题不大,我们先看看 Chrome 眼中的闭包是长怎么样的。
举个例子:
相信很多人都知道,函数
foo
就是一个闭包,通过 Chrome 断点调试可以从视角感知。但是我们稍微修改一下,
此时
a
是全局上下文的变量,尽管对于函数bar
来说a
属于自由变量,但它不是foo
函数上下文内声明的变量,因此foo
就不是闭包。总结:在函数
A
(如foo
)中存在某个函数B
(如bar
,且必须是在A
中定义的),且B
内至少引用了A
中的一个“变量”,那么函数A
就是一个闭包。请注意,与函数
B
的调用方式没关系。无论B
是在foo
内部被调用,还是作为返回值返回,然后在别处调用。再看一个例子:
上述这个示例,是为了提醒
B
对A
中的某个“变量”(指变量、函数、arguments
、形参等)的引用,不仅仅是通过var
、function
、let
、const
、class
等关键字显式声明的,还可以是arguments
对象、形参。换句话说,就是 AO 中的所有变量。再看,下面示例中
foo
是闭包吗?答案是 NO。前面总结过一个函数要成为闭包,该函数(
foo
)内部必须存在另外一个函数(fn
),且fn
内需要foo
中的某个变量。那不正好引用了foo
中的变量a
吗?显然,这理解是错误的。根据词法作用域可知,函数
bar
的作用域链[[scope]]
在声明时就已确定且不可变,只含GlobalContext.VO
,因此当查找自由变量a
时,当bar
的 AO 内查不到,下一步是前往全局对象下查找,于是就找到了a
其值为"global"
。所以fn
内部对foo
构成不了引用,因此foo
就不是闭包。综上所述,Chrome 浏览器眼中的闭包应该是这样的:
在某个函数
A
中存在另一个函数B
(函数B
必须是在函数A
中定义的),而且B
内至少引用了A
中的一个变量,那么当B
在任意地方被调用时,函数A
就是一个闭包。其实,我认为概念不是很重要的...
更多示例
前面的示例,都相对比较简单和清晰的。再看多几个吧。
关于 Chrome 浏览器调试,在 Source 选项卡进行断点调试时,可以看到作用域、闭包的变化。
示例一
请问以下示例会不会产生闭包?(这道题不是考你
this
指向哈,别搞错了)答案是 NO。我们可以在控制台看到。
然后再修改下,当
obj.sayHi()
返回的匿名函数被调用时,存在对obj.sayHi
方法的引用。因此obj.sayHi
就是一个闭包。我们知道箭头函数内部不存在
this
,因此无论obj.sayHi()
返回的匿名箭头函数怎样调用,最终this
都指向obj
对象。但我的疑问在于,以下示例会不会产生闭包?答案还是 NO。其实我这里是有个疑问的,按道理箭头函数不存在
arguments
和this
对象,若在监听函数内访问这两个对象,都应该产生闭包。但事实是,this
引用不会使得sayHi
称为闭包。但是若箭头函数内引用了arguments
对象,则会产生闭包。这一点要注意下!示例二
经典面试题,哈哈!
上述示例打印结果是:
3、3、3
(时间间隔一秒)。如果要每间隔一秒分别输出:1、2、3
,怎么处理?解决方案很简单。解决方法一:
给
setTimeout
披一个函数,即多一层作用域。我就不打断点了,直接从执行过程分析:
解决方法二:使用
let
来声明变量i
。首先请注意
for
语句两种方式的区别,如下:三个作用域的体现:
我认为的闭包
尽管 Chrome 浏览器不认同我的说法,也不会影响我理解和使用闭包,因为我已经知道了作用域链与闭包直接相关。
我在学习闭包的之前,先整体了 JS 整个加载、编译和执行过程。其实学习还是其他,都应该从宏观和微观的角度分析。它们的过程是循序渐进的。
我猜,可能还有挺多有一定经验的 JSer 不知道 JS 脚本是按块加载的。按块加载什么意思?比如我们的网页有两个 JS 脚本(即两对
<script>
标签)。JS 引擎会先对其中一块进行编译与执行的过程,完成之后,才开始对下一个脚本进行编译与执行。假设你不了解,可能误以为 JS 引擎会通篇扫描所有脚本的语法,然后再按顺序(或不按顺序)执行。这是不对的。因此学习闭包也是一样的道理,请先了解 JavaScript 代码从编译到执行的过程。
等这些内容都属性之后,再结合本文或其他大佬的文章,闭包就自然而然就懂了。如果跳过以上内容,直接看闭包,我认为是很难理解的,即使好像当时看懂了,但很快就会忘了。
The end.