循环与闭包

805 阅读3分钟

关键词:IFFE、作用域、闭包、异步

如果下面这些例子也困扰了你很久,那么请跟随本文一探究竟吧!假如你可以全部回答正确,那么恭喜你,这篇文章你可以直接跳过了!

for 循环 + setTimeout

🌰 例 1

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
  • 输出结果:1s 之后,输出 5 5 5 5 5
  • 分析:setTimeout 是异步的,会在循环结束后执行,输出结果是循环结束时 i 的最终值。

每次执行一次循环都将回调函数放入延迟队列,一共放入 5 个回调函数。尽管 5 个函数是分别定义的,但它们共享全局作用域,实际上只有一个 i,所有函数共享一个 i 的引用。

for 循环执行结束后,开始执行回调,此时 i 取值为 5,所以打印了 5 个 5 出来。

But!这并不是我们想要的效果,其实我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。

所以问题就是,我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域,IIFE 登场(关于 IIFE 具体内容见第二部分)

IIFE 会通过声明并立即执行一个函数来创建作用域。

🌰 例 2

for (var i = 0; i < 5; i++) {
  (function () {
    setTimeout(function timer() {
      console.log(i);
    }, 1000);
  })();
}
  • 输出结果:1s 之后,还是输出 5 5 5 5 5
  • 分析:在每次循环中都创建了一个 IIFE,都会创建创建一个自己的作用域。但是作用域内并没有自己的变量,所以仅仅将它封闭起来是不够的。

🌰 例 3

for (var i = 0; i < 5; i++) {
  (function () {
    // 作用域内有自己的变量,用来在每个迭代中储存 i 的值
    var j = i;
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })();
}
// 或这面这样写也可以,将i传递进去
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })(i);
}
  • 输出结果:1s 之后,输出 0 1 2 3 4,终于对了!

总结:在循环内部,使用 IIFE 会为每个循环都生成一个新的作用域,且每个作用域中的 i 是不同的。它使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

🌰 例 4

按照例 3,我们使用 IIFE 在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。 let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (var i = 0; i < 5; i++) {
  let j = i; // 闭包的块作用域!
  setTimeout(function () {
    console.log(j);
  }, 1000);
}

// 请背下来 ⬇️ ⬇️
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

// 如果想要每隔1s输出一次,可以这样写
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}
  • 输出结果:1s 之后,输出 0 1 2 3 4 👌

而且,变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。块作用域和闭包联手便可天下无敌~

下面是一道思考题 👇

🌰 例 5

var test = function () {
  var arr = [];
  for (var i = 0; i < 5; i++) {
    arr.push(function () {
      return i * i;
    });
  }
  return arr;
};
var test1 = test(); // [function,function...]
console.log(test1[4]()); // 25

test 函数执行的时候给 arr 存了 5 个函数,其中 i 都是全局作用域的变量 i,并返回了包含五个函数的数组。 当执行数组中任何一个函数的时候,全局作用域的变量 i 的值为循环结束时 i 的最终值是 5,所以返回 25。

立即执行函数表达式 IIFE - Immediately Invoked Function Expression

先来看下普通函数,它可以将内部的变量隐藏起来,外部作用域无法访问。

var a = 2;
function foo() {
  var a = 3;
  console.log(a); // 3
}
foo();
console.log(a); // 2

但如果我们只是想隔离一些变量,不想声明函数(额外的声明函数会污染全局作用域),也不需要函数名,同时又想自动执行呢?

// 函数表达式,而不是函数声明
(function foo1() {
  var a = 3;
  console.log("inner", a); // 3
})();
  1. 关于作用域的区别:foo 被绑定在所在作用域中,foo1 被绑定在 函数表达式自身的函数中,外部不能访问!且不会污染外部作用域
  2. 关于函数表达式:可以是匿名的,但函数声明不行(在 JS 中非法)
  3. 通过传参的方式,可以改进代码风格
var a = 2;

(function IIFE(global) {
  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2
})(window);
  1. 可以倒置代码的运行顺序。将需要运行的函数放在第二位,在 IIFE 执行之后当作参数传递进去。这种模式在 UMD(Universal Module Definition)项目中被广泛使用
var a = 2;
(function IIFE(def) {
  def(window);
})(function def(global) {
  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2
});
  1. 那 IIFE 是闭包吗?

仁者见仁吧.. IIFE 并不是在它的词法作用域以外执行的,内部变量的值通过普通的作用域查找也可以找到,而非闭包被发现的。 尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包。--《你不知道的 JS-上》

完!!撒花 ✿✿ ヽ(°▽°)ノ ✿