重学JavaScript(6) - 闭包

111 阅读3分钟

首先引用MDN给的定义:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

从定义中可以得出,形成闭包有两个必要的条件:

  • 有一个函数
  • 该函数引用了其周围环境(所在执行上下文,及其上层执行上下文)的变量对象

样例

样例1

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}
​
var myFunc = makeFunc();
myFunc();

在函数makeFunc的执行上下文(EC)中有一个活动对象(AO),如下

AO = {
    arguments: {
        0: undefined,
        length: 0
    }
    name: undefined,
    displayName: <displayName reference>,  // 表示displayName的地址引用
}

当displayName函数被返回并执行时,会引用makeFunc函数执行上下文中的name变量,导致makeFunc的执行上下文的生命周期一直无法结束,造成闭包。

在大多数理解中,包括许多著名的书籍,文章里都以函数displayName的名字代指这里生成的闭包。而在chrome中,则makeFunc以代指闭包。

样例2

var fn = null;
function foo() {
  var a = 2;
  function innnerFoo() {
    console.log(a);
  }
  fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
​
function bar() {
  fn(); // 此处的保留的innerFoo的引用
}
​
foo();
bar(); // 2

innnerFoo函数引用了foo函数执行上下文中的a变量,所以当将innnerFoo函数的引用,赋值给全局变量中的fn,就形成了闭包。当fn在bar函数中执行时,也能够访问到foo函数执行上下文中的a变量。

样例3

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

控制台输出是多少呢?

在弄清楚这段代码前,我们先搞明白setTimeout。

  • setTimeout有两个参数,第一个参数为一个函数,我们通过该函数定义将要执行的操作。第二个参数为一个时间毫秒数,表示延迟执行的时间。
  • setTimeout函数时等到当前执行上下文中所有可执行代码执行完毕之后,才会开始执行由setTimeout定义的操作
setTimeout(function () {
  console.log(a);
}, 0);
​
var a = 10;
​
console.log(b);
console.log(fn);
​
var b = 20;
​
function fn() {
  setTimeout(function () {
    console.log('setTImeout 10ms.');
  }, 10);
}
​
fn.toString = function () {
  return 30;
}
​
console.log(fn);
​
setTimeout(function () {
  console.log('setTimeout 20ms.');
}, 20);
​
fn();
​

输出结果:

所以我们再重头分析一下开始的setTimeOut代码,当执行环境所有代码执行完成偶,i = 5,然后才开始执行setTimeout的函数,所以最终结果,打印出5个5。

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

那如何实现我们最初想要的效果,打印出1,2,3,4,5呢?除了ES6的let,const标识符,我们还是用到上面学到的闭包。利用闭包将外层的变量对象保存下来。

方式1:

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

方式2:

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

参考

前端基础进阶(六):setTimeout与循环闭包面试题详解