谈一谈 JavaScript 中的闭包

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

先拿 MDN 上的一个简单的 🌰 来说:

1
2
3
4
5
6
7
8
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。
displayName() 是定义在 init() 里的内部函数(仅在 init() 函数体内可用),它没有自己的局部变量,
然而它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name

这个就是最简单的一个闭包实现。


不过要了解闭包,就得先了解 JavaScript 变量的作用域,变量的作用域分为 全局变量局部变量

例如全局变量

1
2
3
4
5
var a='test'
function fn(){
console.log(a)
}
fn() // test

全局变量可以在函数内使用,函数内部的变量就不可以在函数外部被调用了

1
2
3
4
function fn(){
var a = 'test'
}
console.log(a) // Uncaught ReferenceError: a is not defined


函数内部可以使用在外部函数内声明的变量,例如文章开头提到的例子,并且每次函数被执行是在内存里都会开辟一个新的区块,所以每次一执行的函数并不是指向同一个内存地址。

但是有些时候我们需要在函数外部调用这个函数内的局部变量。

这就是Javascript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然内部函数可以读取 外部函数 中的 局部变量,那么只要 内部函数 作为 返回值,我们不就可以在函数外部读取它的 内部变量 了吗!

的确,大都数情况我们都是在 函数内部定义一个函数,并且把这个内部函数 return 出来,从而来达到目的。

🌰 例如这个例子

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

上方的例子就可以看到在把 内部函数 赋值给 外部变量 之后,可以继续在之前的运行结果之上进行加运算。

当然也可以修改被调用的外部函数变量,例如这个累加的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function makeAdder(x) {
var x = x
return function(y) {
x += y
return x;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(5)); // 10
console.log(add5(10)); // 20

console.log(add10(5)); // 15
console.log(add10(10)); // 25

一句话形容闭包

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

所以,在本质上,闭包就是将 函数内部函数外部 连接起来的一座桥梁。

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

ES6 普及之后真的很少有使用到闭包了,constlet 的出现方便了太多了。

最后给你们看一个 阮一峰 老师文章里边的例子,乍一看 nAdd() 我懵了半分钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
  alert(n);
}
return f2;
}

var result=f1();

result(); // 999

nAdd();

result(); // 1000

遇到的疑问

  • 闭包是否一定要被外部变量保存(保持被引用)

    并不是只有被外部变量引用的子函数叫做闭包,没有内存地址没有被保持引用的函数也可以是闭包;

    因此可以把闭包简单理解成”定义在一个函数内部的函数”。

  • 引发问题:变量提升,到底会被提升到哪里?

    以前一直以为 变量提升 会把使用 var 声明的变量提升到全局环境下。但是这次再写demo的时候发现原来是提升当前函数的顶部,并不是到全局下的。

    1
    2
    3
    4
    5
    6
    function a(){
    function b(){
    console.log(str)
    }
    var str='test text'
    }

    在 变量提升 下可以这样理解

    1
    2
    3
    4
    5
    6
    7
    function a(){
    var str
    function b(){
    console.log(str)
    }
    str='test text'
    }