# 一. 基础

1. 什么是闭包

能够访问另一个函数作用域变量的函数。闭包就是函数,只不过是声明在其它函数内部而已。

当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包。

2. 作用

  • 闭包可以访问当前函数以外的变量
  • 即使外部函数已经返回,闭包仍能访问外部函数定义的变量与参数
  • 闭包可以更新外部变量的值

3. 优点

  • 避免全局变量的污染
  • 能够读取函数内部的变量
  • 可以在内存中维护一个变量

4. 使用闭包应该注意什么

  • 代码难以维护: 闭包内部是可以访问上级作用域,改变上级作用域的私有变量,我们使用的使用一定要小心,不要随便改变上级作用域私有变量的值

  • 使用闭包的注意点: 由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄漏。解决方法是,在退出函数之前,将不使用的局部变量全部删除(引用设置为 null ,这样就解除了对这个变量的引用,其引用计数也会减少,从而确保其内存可以在适当的时机回收)

  • 内存泄漏: 程序的运行需要内存。对于持续运行的服务进程,必须及时释放不再用到的内存,否则占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏。

  • this 指向: 闭包的 this 指向的是 window

5. 应用场景

setTimeout 传参、回调、IIFE、函数防抖、节流、柯里化、模块化

# 二. 闭包的应用的注意事项

  1. 内存泄漏(Memory Leak)
function foo() {
  let a = 2;

  function bar() {
    console.log(a);
  }

  return bar;
}

let baz = foo();

baz(); //baz指向的对象会永远存在堆内存中

baz = null; //如果baz不再使用,将其指向的对象释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 三. 闭包的应用

# 1. setTimeout 传参

//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param) {
  alert(param);
}, 1000);

//通过闭包可以实现传参效果
function myfunc(param) {
  return function() {
    alert(param);
  };
}
var f1 = myfunc(1);
setTimeout(f1, 1000);
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. 回调

大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

例如,我们想在页面上添加一些可以调整字号的按钮。可以采用 css,也可以使用:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>test</title>
    <link rel="stylesheet" href="" />
  </head>
  <style>
    body {
      font-size: 12px;
    }
    h1 {
      font-size: 1.5rem;
    }
    h2 {
      font-size: 1.2rem;
    }
  </style>
  <body>
    <p>测试</p>

    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>

    <script>
      function changeSize(size) {
        return function() {
          document.body.style.fontSize = size + "px";
        };
      }

      var size12 = changeSize(12);
      var size14 = changeSize(14);
      var size16 = changeSize(16);

      document.getElementById("size-12").onclick = size12;
      document.getElementById("size-14").onclick = size14;
      document.getElementById("size-16").onclick = size16;
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 3. IIFE(自执行函数)

var arr = [];
for (var i = 0; i < 3; i++) {
  //使用IIFE
  (function(i) {
    arr[i] = function() {
      return i;
    };
  })(i);
}
console.log(arr[0]()); // 0
console.log(arr[1]()); // 1
console.log(arr[2]()); // 2
1
2
3
4
5
6
7
8
9
10
11
12

# 4. 防抖,节流

防抖和节流

# 5. 柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
1
2
3
4
5
6
7
8
9
10
11
12
13

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。

# 6. 模块

一个模块应该具有私有属性、私有方法和公有属性、公有方法。

而闭包能很好的将模块的公有属性、方法暴露出来。

var myModule = (function(window, undefined) {
  let name = "echo";

  function getName() {
    return name;
  }

  return {
    name,
    getName,
  };
})(window);

console.log(myModule.name); // echo
console.log(myModule.getName()); // echo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

"return"关键字将对象引用导出赋值给 myModule,从而应用到闭包。

# 7. 延时器(setTimeout)、计数器(setInterval)

这里简单写一个常见的关于闭包的面试题。

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000 * i);
}//每秒钟输出一个 5,一共输出 5 次
1
2
3
4
5

闭包的解决方法:

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

"setTimeout"方法里应用了闭包,使其内部能够记住每次循环所在的词法作用域和作用域链。

由于 setTimeout 中的回调函数会在当前任务队列的尾部进行执行,因此上面第一个例子中每次循环中的 setTimeout 回调函数记住的 i 的值是 for 循环作用域中的值,此时都是 5,而第二个例子记住的 i 的数为 setTimeout 的父级作用域自执行函数中的 j 的值,依次为 0,1,2,3,4。

# 8. 监听器

var oDiv = document.querySeletor("#div");
oDiv.addEventListener("click", function() {
  console.log(oDiv.id);
});
1
2
3
4

# 典型例子

# 例一

let c = 4;
const addX = (x) => (n) => n + x;
const addThree = addX(3);
let d = addThree(c);
console.log("example partial application", d); //输出 7
1
2
3
4
5

同样效果:

let c = 4;
function addX(x) {
  return function(n) {
    return n + x;
  };
}
const addThree = addX(3);
let d = addThree(c);
console.log("example partial application", d);
1
2
3
4
5
6
7
8
9

# 例二

function fn() {
  let a = 0;
  function func() {
    console.log(a);
  }
  return func;
}

let a = 1;
let sub = fn();

sub(); // 0;
1
2
3
4
5
6
7
8
9
10
11
12

解析:sub 就是 func 这一返回值,func 定义在 fn 内部并且被传递出来了,所以 fn 执行之后垃圾回收器依然没有回收它的内部作用域,因为 func/sub 在使用。sub 依然持有 func 定义时的作用域的引用,而这个引用就叫作闭包。调用 sub 时,它可以访问 func 定义时的词法作用域,因此找到的 a 是 fn 内部的变量 a,它的值是 0。

# 例三

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i);
  };
}

data[0](); //3
data[1](); //3
data[2](); //3
1
2
3
4
5
6
7
8
9
10
11

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
1
2
3
4
5
6

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
1
2
3

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

**方案一:**改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function(i) {
    return function() {
      console.log(i);
    };
  })(i);
}
data[0](); //0
data[1](); //1
data[2](); //2
1
2
3
4
5
6
7
8
9
10
11
12

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
1
2
3
4
5
6

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
1
2
3

匿名函数执行上下文的 AO 为:

匿名函数Context = {
  AO: {
    arguments: {
      0: 0,
      length: 1,
    },
    i: 0,
  },
};
1
2
3
4
5
6
7
8
9

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为 3),所以打印的结果就是 0。

data[1] 和 data[2] 是一样的道理。

方案二:let

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i);
  };
}

data[0](); // 0
data[1](); // 1
data[2](); // 2
1
2
3
4
5
6
7
8
9
10
11

方案三:forEach

var data = [];
var arr = [0, 1, 2];
arr.forEach(function(i) {
  data[i] = function() {
    console.log(i);
  };
});
data[0](); // 0
data[1](); // 1
data[2](); // 2
1
2
3
4
5
6
7
8
9
10