# 作用域闭包

注意

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

# 闭包

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

var baz = foo()

baz() // 2 闭包效果
1
2
3
4
5
6
7
8
9
10
11

函数 bar() 的词法作用域能够访问 foo() 的内部作用域。 在这个例子中,将 bar 所引用的函数对象本身当做返回值。在 foo() 执行后,其返回值(bar函数)赋值给变量 baz 并调用 baz(), 实际上只是通过不同的标识符引用调用了内部的函数 bar()。在这个例子中,bar 在自己定义的词法作用域以外的地方执行

function foo () {
  var a = 2
  function baz () {
    console.log(a)
  }

  bar(baz)
}

function bar (fn) {
  fn() 
}
1
2
3
4
5
6
7
8
9
10
11
12
var fn 
function foo () {
  var a = 2
  function baz() {
    console.log(a)
  }

  fn = baz // 赋值给全局变量
}

function bar () {
  fn()
}
foo()
bar() // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

function wait (message) {
  setTimeout(function timer() {
    console.log(message)
  }, 1000)
}

wait('你好闭包')
1
2
3
4
5
6
7

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

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

# 循环和闭包

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

这个循环的终止条件是 i 不在 <= 5。条件首次成立时 i = 6 。因此输出显示的是循环结束时 i 的最终值。延迟函数的回调会在循环结束时才执行。当定时器运行时即使每个迭代中执行的是 setTimeout(..., 0) 所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6 来。

  • 代码中到底有什么缺陷导致他的行为和语义所暗示的不一致呢?

缺陷是我们试图假设循环中的每个迭代在运行时都会给自己捕获一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中 因此实际上只有一个 i。

我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

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

此时拥有了更多的词法作用域了,每个延迟函数都会将IIFE(自执行匿名函数)在每次迭代中创建的作用域封闭起来。如果作用域是空的,那么仅仅是将他们封闭是不够的,需要传参数。

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

使用 IIFE 在每次迭代时都创建一个新的作用域,换句话说每次迭代都需要一个块作用域。

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