如何输出1-5题详解

如何输出1-5

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

分析

i的作用域链:

timer scope -> global scope

i变量会循着以上的作用域链来查找,当然此时是在global作用域链找到了i,但是setTimeout是异步的,会在1s、2s、3s、4s、5s时运行,这个时候都去找全局的i,此时循环早就完成了,i变为6,所以输出5个6,解决问题的关键有两种思路

解决问题的关键

为每次循环的函数保存此次循环的变量状态

1.利用实参

PS: 参数列表 在ES6时,已经被明确定义为一个作用域

在timer scope函数体内查找i变量后,在向上查找之前,会查找函数实参列表中的变量,我们将i传入,并在调用函数时使用这个i,每个函数实例能够保存自己的实参状态,就能解决i的共享问题

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

2.产生作用域来保存状态

我们知道,前面的问题是状态最终都指向了global上的i,作用域可以保存变量的状态,如果在timer和global作用域之间添加一个新的作用域,并让这个中间作用域持久化(形成闭包),那么也能解决当前的i共享问题

  • 使用IIFE产生闭包
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}
// 1 2 3 4 5

分析

(function(){})()

上述代码称之为IIFE(立即执行表达式),可以在不污染现有作用域情况下创建一个新的作用域(函数内部),在循环过程中,每个IIEF中的setTimeout回调函数都因为引用了j变量而产生了闭包效应,j的即时状态得以保存,问题迎刃而解。

  • 使用bind产生闭包
for (var i = 1; i <= 5; i++) {
    setTimeout(function timer(i) {
      console.log(i)
    }.bind(null, i), i * 1000)
}

跟上面类似,bind在内部产生了一个闭包,能够在循环中保存当前循环的i的状态,bind的核心实现如下

Function.prototype.bind = function (context) {
  var fn = this
  return function  ()  {
    return fn.apply(context, args)
  }
}
  • 救世主let
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

我们使用前面的方法,都需要改动函数本身,而let语法可以说是js帮我们做了产生作用域的操作,而又不需要改动太多,仅仅一个符号改变即可享受,美滋滋

let的babel转译结果

var _loop = function _loop(i) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
};

for (var i = 1; i <= 5; i++) {
  _loop(i);
}

babel在这种情况下转译也是利用一个函数产生了一个独立的作用域,且生成了闭包

小结

此问题的核心在于利用作用域和闭包保存瞬时状态,利用函数参数列表和产生作用域都是基于此思路,特别是在ES6,函数参数列表被认为是一个实质的作用域,两个方法的实质都是一样的。

posted @ 2019-06-13 15:37  兴趣使然的Geek  阅读(311)  评论(0编辑  收藏  举报