JavaScript函数II 执行时机

所谓运行机制与执行时机

  1. 了解进程与线程

    • 进程是系统资源分配的最小单位
    • 进程包括进行中的程序和程序所使用到的内存与系统资源
    • 线程是cpu调度的最小单位
    • 线程是程序中的一个执行流,一个进程中可以有多个线程
  2. JavaScript是一门单线程语言

    • 浏览器是多进程的,每打开一个tab页面,一般来说就会产生一个Renderer进程
    • 而浏览器同时只允许一个JS引擎线程运行JS程序,所以JavaScript是单线程运行的
    • JavaScript之所以是一门单线程语言,与它的用途有关,避免了很多复杂的同步问题
  3. 了解同步、异步与回调

    • JS任务分为同步任务和异步任务
    • 同步任务在主线程(即JS引擎)上排队执行,只有前一个任务执行完毕才会执行下一个任务
    • 异步任务不进入主线程,会交给相应的异步处理模块处理异步需要的操作,处理完毕后会在任务队列中注册事件回调函数
    • 当主线程执行栈任务清空后,会自动读取任务队列将回调的异步任务添加到执行栈中开始执行

    上述内容只是非常简单的列举了几点,有关于同步异步事件循环宏微任务的详细内容暂时不在本篇篇幅内赘述,但是可以参考阅读一下这篇文章:「硬核JS」一次搞懂JS运行机制

  4. 何谓「执行时机」?

    总得来说,当定义但并没有调用的时候,函数是不会执行的。而调用才是真正的执行了函数,且调用函数的时机不同,结果不同

举例分析

  1. 为什么下列代码会打印出「6 6 6 6 6 6」

    1
    2
    3
    4
    5
    6
    
    let i = 0
    for(i = 0; i<6; i++){
        setTimeout(()=>{
            console.log(i)
        },0)
    }
    

    在这部分代码中,我们可以根据顺序来看,首先使用let指令声明了一个变量i,然后进入了for循环,判断条件为i<6。首次循环至内部,触发setTimeout函数,这对JS引擎来说是一个异步任务,并不会在主线程上执行,而是交给相应的定时器触发线程处理时间间隔,计时完毕后,将内部需要执行的函数回调给事件触发线程,并列入事件队列等待执行。需要注意的是,异步任务并不会“堵塞”同步任务的执行进程,所以for循环还会继续执行,累加后进入下一次循环。

    i=1进入下一次循环时,就会发现又遇到这个setTimeout函数了!是的,它将再一次从主线程中“抽离”,经由异步处理进入事件队列等待执行。也就是说,直到i=6跳出这个for循环时,for“空转”了6次。

    现在主线程的同步任务全部执行完毕了,JS引擎便会问询事件触发线程,将事件队列中的回调函数加入主线程执行栈开始执行。事件队列中一共有6个需要执行的回调函数,代入后此时我们的最终值i=6,所以会打印出「6 6 6 6 6 6」。

  2. 如何让这列代码打印出「0 1 2 3 4 5」

    1
    2
    3
    4
    5
    6
    
    //在ES6的语法更新中,使用for let可以让代码打印出符合「常识」的结果
    for(let i = 0; i<6; i++){
        setTimeout(()=>{
            console.log(i)
        },0)
    }
    

    通过「6 6 6 6 6 6」的“魔咒”,应该不难发现我们最后执行的i只有一个。而我们希望的是,每一次进到循环内部在i分别等于 0 1 2 3 4 5时,它的值都会好好的传给延迟函数,也就是封闭每次迭代的作用域

    所以let出现了,它不仅劫持了这块作用域(甚至劫持了整整6次😂),而且在for循环的头部使用let声明还存在着一个特殊的行为:每次迭代都会声明,每个迭代都会使用上一个迭代结束时的值来初始化变量。

  3. 除了使用for let,还有什么方法可以打印出「0 1 2 3 4 5」

    这么说也许还有些令人困惑🤔,那么不用for let头部完成这个操作应该如何呢?

    1
    2
    3
    4
    5
    6
    
    //让我们回到最初的起点
    for(i=0; i<6; i++){
        setTimeout(()=>{
            console.log(i)
        },0)   
    }
    

    我们的目标是封闭函数所在的作用域,获得每次迭代的值,使最后回调函数执行的时候依旧可以引用到我们亲手封闭的作用域

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    for(i=0; i<6; i++){
        (function(flag){
                setTimeout(()=>{
                    console.log(flag)
                },0)
            })(i)
    }
    //这里使用了立即执行函数表达式(IIFE)来完成
    //通过循环将i的当前值作为参数传入function,建立一个匿名函数对其内容进行包装(创建并封闭作用域)
    //当然别忘了执行,不然为啥叫IIFE,不执行只是定义了而已🤪
    

    再优化一点点,相信就可以看出let在扮演什么样的角色了⬇️

    1
    2
    3
    4
    5
    6
    7
    8
    
     for(i=0; i<6; i++){
        let flag = i
        setTimeout(()=>{
            console.log(flag)
         },0)
    }
    //是的,let自己就创建了一个封闭作用域
    //并且声明了一个在这作用域中有生命力的变量
    

    就是这样。

    写到这里我虽然没有明确的说出来,但是这里面充满了闭包。而这也只是我所认知到的闭包(的一部分),即一个内部函数无论以什么方式被引用至作用域外,它仍然可以访问定义自己的作用域。我听见闭包现在正对我说:“孩子,听叔一句劝,这里的水太深,你把握不住。”

    等我把握得住了,再来详细说一说。 把握的不太住也来说一说,指路👉🏻《关于JavaScript 作用域&闭包》🥳