闭包:有权访问另一个函数作用域中的变量的函数 ——红宝书定义奉上。
匿名函数:没有名字的函数。
它俩关系,如果匿名函数刚好访问了另一个函数作用域中的变量,那它就也是闭包了。
先来个经典闭包镇楼:
为什么js会有闭包这样的存在呢?毕竟我们都知道C、C++这样高贵的语言是不可以在一个函数中定义另外一个函数。JS稀里糊涂的地方多了去了,但闭包还真的不是一个迷糊的存在。
-
首先:在JS里,函数首先是对象,既然是对象,就可以定义属性和方法,你可以定义一个变量,当然可以定义一个函数,所以:JavaScript 语言允许在函数内部定义新的函数。
-
其次:在JS里,内部函数可以访问父函数中定义的变量。这个比较好理解,就好像面向对象的语言,爸爸的家产,儿子打过招呼了就可以用。如果还有疑问可以搜一下JS作用域链看,总之就是自己没有就沿着作用域链往上找,爸爸没有找爷爷,爷爷没有找老爷爷,找到头儿了还没有那就是老祖宗没给你留这份儿家产,这就要给你报错了!!!
- 最后:在JS中,函数是一等公民,JS 的函数可以赋值给一个变量,也可以作为函数的参数,还可以作为函数的返回值。
一等公民就是: 如果某个编程语言的函数可以和它的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。
基于以上三点:闭包这样伟大的发明就产生了。但,看过红宝书的都知道(没看过的也知道):闭包并不推荐频繁使用,因为会造成内存泄漏。那闭包为什么会造成内存泄漏呢?
依然以这段代码为例:
- 当调用 foo 函数时,foo 函数会将它的内部函数 inner 返回给全局变量 f;
- foo 函数执行结束,执行上下文就会被销毁了,这时候变量d按理来说也就不存在了。
- 但依然活跃的 inner 函数已经被保存在全局变量中,而且还引用了foo 函数作用域中的变量 d,那变量d到底是被回收了还是没有被回收呢?
变量d它当然还要好好的活在作用域中。这就造成了大家熟知的闭包会造成内存泄漏。
但,变量d是怎么存在作用域中的呢?
我们都知道浏览器在运行JS代码时,会先编译后运行,以V8为例,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,而且还会占用大量内存,所以主流浏览器都采用了惰性解析方法。
惰性解析是指如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码,只有在运行到这个函数的时候才会进到函数内部再一次进行解析。
浏览器虚拟机的惰性解析机制如果遇到闭包,就尴尬了!因为当执行到 foo 函数时,V8 只会解析 foo 函数,并不会解析内部的 inner 函数,那么这时候 V8 就不知道 inner 函数中是否引用了 foo 函数的变量 d。
不知道没关系,这时候还有一个叫预解析器的东东,虚拟机在解析顶层代码的时候,遇到函数声明,预解析器并不会直接跳过,而是对该函数做一次快速的预解析,其主要目的有两个:
-
判断当前函数是不是存在一些语法上的错误;
function foo(a, b) { {/} //语法错误} -
最重要的就是!检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,即便foo函数的上下文被推出了栈,也能在堆中找到变量d,这样就解决了闭包所带来的尴尬!
所以当虚拟机解析函数的时候,还要判断该函数的内部函数是否引用了当前函数内部声明的变量,如果引用了,那么需要将该变量存放到堆中,即便当前函数执行结束之后,也不会释放该变量。
暂时先总结到这里,日后有新的想法再补充~