闭包

什么是闭包

闭包的定义其实很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

1
2
3
4
5
6
7
function A() {
let a = 1;
function B() {
console.log(a);
}
return B;
}

变量的作用域

要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript 语言的特殊之处,就在于函数内部可以直接读取全局变量。

1
2
3
4
5
var n = 999;
function f1() {
console.log(n);
}
f1(); // 999

另一方面,在函数外部自然无法读取函数内的局部变量。

1
2
3
4
function f1() {
var n = 999;
}
console.log(n); // 报错

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用 var 命令。如果不用的话,你实际上声明了一个全局变量!

1
2
3
4
5
function f1() {
n = 999;
}
f1();
console.log(n); // 999

有时候需要得到函数内的局部变量,在函数的内部,再定义一个函数。

1
2
3
4
5
6
function f1() {
n = 999;
function f2() {
console.log(n); // 999
}
}

在上面的代码中,函数 f2 就被包括在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。但是反过来就不行,f2 内部的局部变量,对 f1 就是不可见的。这就是 Javascript 语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然 f2 可以读取 f1 中的局部变量,那么只要把 f2 作为返回值就可以在 f1 外部读取它的内部变量了。

1
2
3
4
5
6
7
8
9
function f1() {
n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999

使用闭包的注意点

1.由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

说说你对闭包的理解

使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在 js 中,函数即闭包,只有函数才会产生作用域的概念
闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
闭包的另一个用处,是封装对象的私有属性和私有方法
好处:能够实现封装和缓存等;
坏处:就是消耗内存、不正当使用会造成内存溢出的问题

闭包的作用有

1.封装私有变量 2.模仿块级作用域(ES5 中没有块级作用域) 3.实现 JS 的模块

经典面试题,循环中使用闭包解决 var 定义函数的问题

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
解决办法两种,第一种使用闭包

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}

第二种就是使用 setTimeout 的第三个参数

1
2
3
4
5
6
7
8
9
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j);
},
i * 1000,
i
);
}

第三种就是使用 let 定义 i 了

1
2
3
4
5
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

因为对于 let 来说,他会创建一个块级作用域,相当于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}

闭包的缺点

  1. 由于闭包会是的函数中的变量都被保存到内存中,滥用闭包很容易造成内存消耗过大,导致网页性能问题。解决方法是在退出函数之前,将不再使用的局部变量全部删除。

  2. 闭包可以使得函数内部的值可以在函数外部进行修改。所有,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。