闭包的定义
维基百科的解释
- 闭包(英语:Closure),又称词法闭包(Lexical Closure) 或函数闭包(function closures)
是在支持头等函数
的编程语言中,实现词法绑定的一种技术。 - 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)。
- 闭包跟函数最大区别在于,当捕捉闭包的时候,它的
自由变量
会在捕捉时被确定,这样即使脱离了
捕捉时的上下文,它也能照常运行。 - 闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JS 中有闭包。
因为 JS 中有大量的设计是来源于 Scheme 的;
MDN 的解释
- 一个函数和对其
周围状态
(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
(closure)。 - 也就是说,闭包让你可以在一个内层函数中
访问
到其外层函数的作用域; - 在 JS 中,每当创建一个函数,闭包就会在函数创建的时候被创建出来;
总结
- 一个普通的函数,如果它可以
访问外层作用域的自由变量
,那么这个函数就是一个闭包. - 从广义的角度来说: JS 中的函数都是闭包.
- 从狭义的角度来说 JS 中一个函数,如果访问了外层作用域的变量,那么它就是一个闭包.
V8 如何实现闭包
- V8 在执行 JS 代码时,需要经过
编译
和执行
两个阶段,编译过程
指 V8 将 JS 代码转换为字节码或者二进制辑器代码的阶段,而执行阶段
则是指解释器执行字节码,或者是 CPU 直接执行二进制机器码的阶段。总的流程:编译(生成 AST、Scope:变量提升)-> bytecode -> 执行
在编译 JS 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,主要原因有以下两点:
- 如果一次性解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。
- 解析完成的
字节码
和编译之后的机器码
都会放在内存中,如果一次性解析和编译所有的 JavaScript 代码,那么这些中间会一直占用内存
。基于以上的原因,所有主流的 JavaScript 虚拟机都是实现了惰性解析。
惰性解析:是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成AST
和字节码
,而仅仅生成顶层
代码的 AST 和字节码。
JavaScript 闭包的三个特性
- JS 语言允许在函数内部定义新的函数。
function foo() {
function bar() {}
bar();
}
- 可以在内部函数中访问父函数中定义的变量。
var msg = "hello";
// bar函数的父函数,词法作用域
function foo() {
var msg = "world";
// foo的内部函数
function bar() {
return msg + "~~~";
}
bar();
}
- 因为函数是一等公民,所以函数可以作为返回值。
function foo() {
return function bar(x, y) {
var z = x + y;
return z;
};
bar();
}
const f = foo();
闭包给惰性解析带来的问题
下面我们就来使用三个特性组装的一段经典的闭包代码:
function foo() {
var msg = "world";
return function bar(x, y) {
var z = x + y + msg;
return z;
};
}
var f = foo();
我们可以分析下上面这段代码的执行过程:
当调用 foo 函数时,foo 函数会将它的内部函数 bar 返回给全局变量 f;
然后,foo 函数执行结束,执行上下文被 V8 销毁;
虽然 foo 函数的执行上下文被销毁了,但是依然存活的 bar 函数引用了 foo 函数作用域中的变量 msg;
按照通用的做法,msg 已经被 V8 销毁了,但是由于存活的函数 bar 依然引用了 foo 函数中的变量 msg,这样会带来两个问题:
当 foo 执行结束时,变量 d 该不该被销毁?如果不应该被销毁,那么应该采用什么策略?
如果采用了
惰性分析
,那么当执行到 foo 函数时,V8 只会解析 foo 函数,并不会解析内部的 bar 函数,那么这时候 V8 就不知道 bar 函数中是否引用了 foo 函数的变量 msg;
正常做法:应该时 foo 函数的执行上下文虽然被销毁了,但是 bar 函数引用的 foo 函数中的变量却不能被销毁,那么 V8 就需要为这种情况特殊处理,需要保证即使 foo 函数执行结束,但是 foo 函数中的 msg 变量依然保持在内存中,不能随着 foo 函数的执行上下文被销毁掉。
那么怎么处理呢?
在执行 foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的 bar 函数,但是 V8还是需要判断 bar函数是否引用了 foo函数 中的变量
,负责处理这个任务的模块是预解析器
。
预解析器如何解决闭包所带来的问题?
V8 引入与解析器,比如当解析顶层代码
的时候,遇到了一个函数,那么预解析器并不会跳过该函数
,而是对该函数做一次快速的预解析
,其主要目的有两个。
- 判断当前函数是不是存在一些语法上的错误,如下面这段代码:
function foo(x, y) {
{msg/} // 语法错误
}
var x = 1;
var y = 2;
foo(1, 8);
在预解析过程中,预解析器发现了语法错误,那么就会向 V8 抛出语法错误;
- 预解析器另外的一个重要的功能就是
检查函数内部是否引用了外部变量
,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中
,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题。