JavaScript 系列之闭包(一)

309 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

一、闭包的定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

简单来讲:

闭包就是能够读取其他函数内部变量的函数

在函数 A 中还有函数 B,函数 B 调用了函数 A 中的变量,那么函数 B 就称为函数 A 的闭包。

function foo() {
  let a = 2;
  function bar() {
    console.log(a);
  }
  bar();
}
foo(); // 2

二、循环和闭包

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log("i: ", i);
  }, 1000);
}
console.log(i);

// 输出
// 5;
// i: 5;
// i: 5;
// i: 5;
// i: 5;
// i: 5;

image.png

要理解上述代码,首先我们得了解一下概念:

  • JS 分为同步任务异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

上述代码中使用定时器,当 JS 引擎线程执行到该段代码,便把定时器放到定时器线程去计时,此时 JS 引擎线程执行同步栈里面的任务。当定时器计时完成之后,将回调函数推入消息队列。等待栈中的代码执行完毕之后会去读取消息队列中的事件。

由于 JS 的函数作用域,当回调函数被推入消息队列的时候没有带上参数。for 循环结束之后,因为 i 是用 var 定义的,所以 var 是全局变量(这里没有函数,如果有就是函数内部的变量),这个时候的 i 是 5。

image.png

如何解决?

// let 是块级作用域,当前块是循环体
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log('i: ',i);
  }, 1000);
}
console.log(i);

// 输出
// 5
// i:   0
// i:  1
// i:  2
// i:  3
// i:  4
//这是函数级作用域,利用闭包模拟块级作用域
for (var i = 0; i < 5; i++) {
  (function(){
    var j = i;
    setTimeout(function() {
      console.log('i: ',j);
    }, 1000);
  })();
}
console.log(i);

// 输出
// 5
// i:  0
// i:  1
// i:  2
// i:  3
// i:  4
// 或者采取传参的方式,使用了闭包
for (var i = 0; i < 5; i++) {
  (function(j){
    setTimeout(function() {
      console.log('i: ',j);
    }, 1000);
  })(i);
}
console.log(i);

// 输出
// i is not defined
// i:  0
// i:  1
// i:  2
// i:  3
// i:  4
//  setTimeout 的第三个参数
for (var i = 0; i < 5; i++) {
  setTimeout(function timer(j) {
    console.log('i: ', j);
  }, 1000, i);
}
console.log(i);

// 输出
// i is not defined
// i:  0
// i:  1
// i:  2
// i:  3
// i:  4