JS 闭包

什么是闭包

1
2
3
4
5
6
7
8
function A() {
var a = 1
B = function () { //B没有赋值的话代表作用域是全局的,才能在函数外调用B函数
console.log(a)
}
}
A()
B() // 1

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量

使用闭包的优缺点

避免命名空间污染

1
2
var num = 1;
(function count() { return num; })() // 1
1
2
3
4
5
var count = function () {
var num = 1;
return function () { return num; }
}
count()() // 1

虽然两段代码表达的意思一样,但是你们发现了没有第一段代码声明了一个全局变量num挂载到window中,大项目多人开发时可能会有变量名冲突的情况,容易造成命名空间污染,闭包解决了

随时可以使用但是消耗内存

我们先来看看引用计数的垃圾回收机制

跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,如果这个值再被赋值给另一个变量,则引用次数加1。相反,如果一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收

闭包创建了循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0

也就是说我们可以随时调用闭包,但是变量被保存起来没有被销毁,非常消耗内存

2020-2-3号补充

1
2
3
4
5
6
7
8
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();

这段代码看起来和嵌套作用域中的示例代码很相似。基于词法作用域的查找规则,函数bar() 可以访问外部作用域中的变量 a(这个例子中的是一个 RHS 引用查询)。

这是闭包吗?技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部分!)

词法作用域看我这篇博客

从纯学术的角度说,在上面的代码片段中,函数 bar() 具有一个涵盖 foo() 作用域的闭包(事实上,涵盖了它能访问的所有作用域,比如全局作用域)。也可以认为 bar() 封闭了foo()的作用域中。为什么呢?原因简单明了,因为 bar() 嵌套在 foo() 内部。

但是通过这种方式定义的闭包并不能直接进行观察,也无法明白在这个代码片段中闭包是如何工作的。我们可以很容易地理解词法作用域,而闭包则隐藏在代码之后的神秘阴影里,并不那么容易理解。

下面我们来看一段代码,清晰地展示了闭包:

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。

在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实际上只是通过不同的标识符引用调用了内部的函数 bar()。

bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。

拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

因此,在几微秒之后变量 baz 被实际调用(调用内部函数 bar),不出意外它可以访问定义时的词法作用域,因此它也可以如预期般访问变量 a。

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}

把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部作用域的闭包就可以观察到了,因为它能够访问 a。

传递函数当然也可以是间接的。

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

前面的代码片段有点死板,并且为了解释如何使用闭包而人为地在结构上进行了修饰。但我保证闭包绝不仅仅是一个好玩的玩具。你已经写过的代码中一定到处都是闭包的身影。现在让我们来搞懂这个事实。

1
2
3
4
5
6
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );

将一个内部函数(名为 timer)传递给 setTimeout(..)。timer 具有涵盖 wait(..) 作用域的闭包,因此还保有对变量 message 的引用。wait(..) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(..)作用域的闭包。

在引擎内部,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个参数也许叫作fn 或者 func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的 timer 函数,而词法作用域在这个过程中保持完整。

这就是闭包。

0%