作用域闭包

JavaScript中闭包无处不在,你只需要识别并拥抱它。

闭包是基于词法作用域书写代码时所产生的自然结果。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

1function foo() {
2  let a = 2;
3  function bar() {
4    console.log(a);
5  }
6  // 将 bar() 函数本身当作一个值类型进行传递。
7  return bar;
8}
9
10const baz = foo();
11// 实际上只是通过不同的标识符引用调用了内部的函数 bar(), bar() 显然可以正常执行,这里它在自己定义的词法作用域以外被执行了。
12baz(); // 2
1function foo() {
2  let a = 2;
3  function baz() {
4    console.log(a);
5  }
6  bar(baz);
7}
8function bar(fn) {
9  fn(); // 这就是闭包
10}
11foo();
1let fn;
2function foo() {
3  let a = 2;
4  function baz() {
5    console.log(a);
6  }
7  fn = baz; // 把baz分配给全局变量
8}
9function bar() {
10  fn(); // 这就是闭包
11}
12foo();
13bar(); // 2
1function foo(message) {
2  // 将一个内部函数timer传递给setTimeout
3  // timer具有涵盖 foo作用域的闭包,还保有对变量message的引用
4  // 在引擎内部,内置的工具函数setTimeout持有对一个参数的引用,这个参数也叫做fn或者func,或者其他类似的名字。在这个函数中就是内部的 timer函数,而词法作用域在这个过程中保持完整。
5  setTimeout(function timer() {
6    console.log(message); // hello, closure
7  }, 1000);
8}
9foo("hello, closure");

:::

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

本质上,无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

循环和闭包

1for (var i = 1; i <= 5; i++) {
2  // 以每隔一秒的频率输出五次6
3  setTimeout(function timer() {
4    console.log(i);
5  }, i * 1000);
6}
1for (var i = 1; i <= 5; i++) {
2  // 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
3  (function (j) {
4    setTimeout(function timer() {
5      console.log(j);
6    }, j * 1000);
7  })(i);
8}

块级作用域和闭包

for循环头部的let声明还有有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每个迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

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

模块

模块模式需要具备两个必要条件:

  1. 必需有外部的封闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)

  2. 封闭函数必需返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态

一个具有函数属性的对象本身并不是真正的模块

一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块