闭包
1.JS中的垃圾回收机制(GC)
函数中的局部变量会经历一个生命周期:
- 当我们定义一个变量时,会为它分配一个内存空间用来储存变量的值;
- 当我们读取或使用这个变量时,会使用它的内存空间;
- 使用完毕时会释放内存空间。
上面生命周期的最后一步,其实就是垃圾回收。而 JS 中存在自动的垃圾回收机制,即GC。
在 JS 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收。
如果两个对象互相引用,而不再被第三者所引用,那么这两个互相引用的对象也会被回收。
通常对于某个变量,我们不需要使用之后,就可以把它的值设置为 null ,这样垃圾回收器就会回收这个变量占用的空间了!
一般来说,当一个函数执行完毕,那么这个局部作用域内的变量就没有存在的必要了,它的内存空间就会释放。
但是实际上有一种情况可以阻止这一进程,那就是 — 闭包。
2.闭包的概念
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
看下面这个例子:
function foo() {
var a = 2;
// 函数bar() 的词法作用域能够访问foo() 的内部作用域。
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
// 在foo() 执行后,其返回值(也就是内部的bar() 函数)赋值给变量baz并调用baz()
// 实际上只是通过不同的标识符引用调用了内部的函数bar()。
// bar() 显然可以被正常执行,而且是在自己定义的词法作用域以外的地方执行了。
以上代码中,在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()
本身在使用。
拜bar()
所声明的位置所赐,它拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()
依然持有对该作用域的引用,而这个引用就叫作闭包。
即:
- 函数在定义时的词法作用域以外的地方被调用时,闭包使得函数可以继续访问定义时的词法作用域。
- 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
3.闭包的特性和应用场景
3.1 闭包的特性
- 被闭包函数访问的父级及以上的函数的局部变量(如范例中的局部变量 i )会一直存在于内存中,不会被JS的垃圾回收机制回收,即数据持久性。
- 闭包函数实现了对其他函数内部变量的访问。(函数内部的变量对外是无法访问的,闭包通过这种变通的方法,实现了访问。)
3.2 闭包的应用场景
- 模拟面向对象的代码风格 - 属性 + 方法
- 使 setTimeOut 方法支持传参
- 封装私有变量 - 闭包可以⽤来创建私有变量,还可以延⻓变量的⽣命周期。⽐如在函数内定义变量,然后get和set⽅法,这样在函数外部就可以通过get、set访问到内部的变量,就实现了一个私有变量。
- 模拟块作用域
- 实现迭代器
3.3 闭包的优缺点
优点:
- 可以减少全局变量的定义,避免全局变量的污染
- 能够读取函数内部的变量
- 在内存中维护一个变量,可以用做缓存
缺点:
- 造成内存泄露:
- 闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
- 解决方法 - 使用完变量后,手动将它赋值为null;
- 闭包可能在父函数外部,改变父函数内部变量的值。
- 造成性能损失:
- 由于闭包涉及跨作用域的访问,所以会导致性能损失。
- 解决方法 - 通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响
4.循环与闭包
…