setTimout-闭包-作用域-任务队列

  1. 请看下面这道题

    function show(){
        for(var i=0;i<5;i++){
            setTimeout(()=>{console.log(i)},0)
        }
    }
    show()
    console.log('start')

    输出结果:start

    5 5 5 5 5

    原因:setTimout的第一个参数属于异步函数,会被存到宏任务队列中,当执行栈执行完毕之后才会执行宏任务队列,又由于var定义的i这个变量时没有作用域块这个概念的,所以在执行栈中一直被++。

  2. 如何让这个函数,按正常顺序输出1 2 3 4 5

2.1这是面试中被经常被问到的,解决办法:使用闭包,并且让闭包立即执行。

function show(){
    for(var i=0;i<5;i++){
        let fn=function(){
            setTimeout(()=>{console.log(i)},0)
        }
        fn(i)
    }
}
show()
console.log('start')
    ```

输出结果:start   0 1 2 3 4

这个里面的闭包函数也可以换个写法,网上常见的:

```javascript
function show(){
    for(var i=0;i<5;i++){
        (function(i){
            setTimeout(()=>{console.log(i)},0)
        }).(i)
    }
}

原理解释:闭包的作用就是,让外部函数可以调用内部函数中的变量。深层一点理解:闭包的作用可以将一个函数中的数据存储在这个函数的上下文环境中,当外部执行执行闭包函数时,任然可以拿到这个原始的数据。执行完毕之后这个数据就被清掉。例子:A函数中定义了B函数并且返回了B函数,那么不管B函数在哪里被调用或如果被调用,它都会保留A函数的作用域。

解释:执行循环的时候,每次碰到setTimeout函数,都会将()=>console.log(i)这行代码给压到任务队列中,最后执行,但是由于这边写的闭包函数时立即执行的,所有i这个变量就被存储到了准确的上下文环境中。这里有五次循环,第一次循环 i=1被保存到上下文环境中,因为i是在setTimeout中才被引用的,同时setTimeout的被压到执行队列。第二次 i=2 被保存到上下文环境中,setTimout被压到执行队列中,如此重复五次。当第一个setTimout被执行的时候,i被准确保存在对应的上下文环境中,所以可以console.log(1),如此重复五次。

2.1 解决方案将 var 改为let

function show(){
    for(let i=0;i<5;i++){
        setTimeout(()=>{console.log(i)},0)
    }
}
show()
console.log('start')

输出结果:

start 0 1 2 3 4

在解释原因之前,let与var的区别有一个非常重要的一点就是:let有块级作用域,并且let在for循环中还有个特殊的行为,这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量,比方5次循环,i会被5次重新声明初始化

for(let i=0;i<5;i++){
    console.log(i)
}

本质上是以下代码:

{
    let i=0
    console.log(i)
    let tem=i+1
    {
        let i=tem
        console.log(i)
        tem=i+1

        {
            let i=tem
            console.log(i)
            tem=i+1

            {
                let i=tem
                console.log(i)
                tem=i+1 

                {
                    let i=tem
                    console.log(i)
                }
            }
        }
    }
}

所以对应的let 方案的本质代码如下:

{
    let i=0
    console.log(i)
    let tem=i+1
    {
        let i=tem
        setTimeout(()=>{console.log(i)},0)
        tem=i+1

        {
            let i=tem
            setTimeout(()=>{console.log(i)},0)
            tem=i+1

            {
                let i=tem
                setTimeout(()=>{console.log(i)},0)
                tem=i+1 

                {
                    let i=tem
                    setTimeout(()=>{console.log(i)},0)
                }
            }
        }
    }
}

执行setTimout的第一个参数,它都会去自己原先的作用域中对应的作用域链中往上找对应的i值,如果离自己最近的一个作用域中没有i值,就会忘上一个作用域中找,遍历作用域链,知道找到为止,如果没有找到,在非严格模式下,全局自己定义一个i,返回undefined

{
    let i=0
    console.log(i)
    {
       setTimeout(()=>{console.log(i)},0)
       i++
       {
        setTimeout(()=>{console.log(i)},0)
        i++

        {
            setTimeout(()=>{console.log(i)},0)
            i++

            {
                setTimeout(()=>{console.log(i)},0)
                i++

                {
                    setTimeout(()=>{console.log(i)},0)
                }
            }
        }
       }
    }
}

这种写法的结果:输出:4 4 4 4 4

这种写法的执行顺序是:let i=0 ,然后 i++ i ++ i++ i++ i++ 最后执行setTimeout,setTimouet在自己本身的作用域中找不到i的声明回往上找到,所以最终所有的setTimeout都找到同一i身上,且值为4

参考:

https://www.jianshu.com/p/3e482748369d?from=groupmessage

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务