美文网首页
js闭包 与事件队列

js闭包 与事件队列

作者: 感觉不错哦 | 来源:发表于2018-11-08 13:59 被阅读93次
    针对闭包相信小伙伴们有很多不同的概念跟理解
    何为闭包,从结构上来讲,闭包就是函数套函数,类似递归这种函数调用函数本身的也算是闭包;当然这是从结构上来看,从闭包的特点来看,递归又不算是闭包
    闭包的作用主要是获取函数内部的局部变量,这也是Javascript语言的特殊之处

    JS 的闭包包含以下要点:

    函数声明的时候,会生成一个独立的作用域
    同一作用域的对象可以互相访问
    作用域呈层级包含状态,形成作用域链,子作用域的对象可以访问父作用域的对象,反之不能;另外子作用域会使用最近的父作用域的对象

    上代码理解闭包,通过实例会更容易理解

       function f1(){
       var n=999;  =>一定要加声明  不然变成全局变量了
       return function f2(){
        console.log(n); 
        }
       }
      f1()()// 999
    

    js中变量只有两种情况,局部变量与全局变量
    这就是函数的作用域链
    我把代码解构一下 方便大家理解

       function f1(){                                ---------------------------------------------
    
       var n=999;
                                                                                     此处都属于f1的作用域 也可以称之父作用域
                return function f2(){   -------------------------
    
                          console.log(n);                      这段就是子作用域
    
                            }-----------------------------------------
        
       }--------------------------------------------------------------------------------------
    
      f1()()// 999
    

    f1()的结果是返回f2 f2()函数执行 打印n 但是此时f2中并没有n
    那么他就会往它的父作用域去找 ,好 此时找到了n 那么此时n为999

       var n=999; 
       function f1(){
       return function f2(){
        console.log(n); 
        }
       }
      f1()()// 999
    

    相同的, 如果f1里不存在n 那么就会再去上一级 也就是window中寻找n 那么此时n就是window中的n

       var n=999; 
       function f1(){
        var n=80;   
       return function f2(){
        console.log(n); 
        }
       }
      f1()()// 80
    

    此时 f1中也有了n 那么 根据前面说的 闭包的特点 子作用域会使用最近的父作用域的对象
    他会取到最近的作用域的属性 此时n为80

       function f1(){
           
       return function f2(){
        console.log(n); 
        }
       }
       
      f1()()// undefined
      var n=80;
    

    注意,作用域只会向上寻找,不会向下,函数的声明位置是无所谓的,这根函数的预解析有关,函数的声明只跟调用有关,此时在函数向上寻找n的过程中没有发现n,其实已经发现了,n等于undefined此时,因为var 变量提升

    通过这个例子,我想大家很容易理解上面特点的第三条

    作用域呈层级包含状态,形成作用域链,子作用域的对象可以访问父作用域的对象,反之不能;另外子作用域会使用最近的父作用域的对象

    那么我们也说过 闭包的作用主要是获取函数内部的局部变量
    我们尝试来提取一下

    function f1(){ 
       return function f2(){
              for(var i=0;i<10;i++){
                 console.log(i)  //0,1,2,3,4,5,6,7,8,9
              } 
       }
    }
    f1()() 
    

    这样我们就可以将子函数的变量给提取出来了

    我们都知道 for循环是异步进行的 如果我们在外面调用此方法,打印出来的是10个10

    待会我们一起分析几道闭包的笔试题来做更深入的了解

    这里我们番外一下,什么是js的 任务队列

    在此之前希望你对promise又简单的了解,不然下方的例子可能看不懂

    首先我们要知道,javascript它是单线程语言

    JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
    JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
    所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

    之所以js是单线程语言

    就是说在一行代码执行的过程中,必然不会存在同时执行的另一行代码,就像使用alert()以后进行疯狂console.log,如果没有关闭弹框,控制台是不会显示出一条log信息的

    亦或者有些代码执行了大量计算,比方说在前端暴力破解密码之类的鬼操作,这就会导致后续代码一直在等待,页面处于假死状态,因为前边的代码并没有执行完。

    所以如果全部代码都是同步执行的,这会引发很严重的问题,比方说我们要从远端获取一些数据,难道要一直循环代码去判断是否拿到了返回结果么?就像去饭店点餐,肯定不能说点完了以后就去后厨催着人炒菜的,会被揍的。

    于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。

    然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。

    比如说打了个车,如果司机先到了,但是你手头还有点儿事情要处理,这时司机是不可能自己先开着车走的,一定要等到你处理完事情上了车才能走。

    相反,如果是公交车,那就不管你了

    因为同步太好理解了,从上向下执行操作,主要讲一下异步事件

    微任务(microtask)与宏任务(macrotask)=>异步事件

    mircotask(微任务)

    promise
    mutation.oberver
    process.nextTick

    marcotask(宏任务)

    setTimeout,setInterval
    requestAnimationFrame
    解析HTML
    执行主线程js代码
    修改url
    页面加载
    用户交互

    有些没见过或者不认识的没关系,因为有些是node.js中的 我这边也主要举例浏览器内的事件队列

        console.log('script start');    第一个打印
        setTimeout(function() {
        console.log('setTimeout');   第五个打印
        }, 0);
        Promise.resolve().then(function() {
        console.log('promise1');       第三个打印
        }).then(function() {
        console.log('promise2');    第四个打印
        });
        console.log('script end');    第二个打印
    

    从这里我们可以看出 微任务(mircotask)它的事件比宏任务(marcotask)优先执行

    解读:

    同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
    当指定的事情完成时,Event Table会将这个函数移入Event Queue。
    主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
    上述过程会不断重复,也就是常说的Event Loop(事件循环)。

            let data = [];
            $.ajax({
                url:www.javascript.com,
                data:data,
                success:() => {
                    console.log('发送成功!');
                }
            })
            console.log('代码执行结束');
    

    ajax进入Event Table,注册回调函数success。
    执行console.log('代码执行结束')。
    ajax事件完成,回调函数success进入Event Queue。
    主线程从Event Queue读取回调函数success并执行。

    微任务和宏任务皆为异步任务,也就是说它们都会进入Event Table,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢?

    我们先来看看setTimeout(宏任务),这个大家应该很熟悉

        setTimeout(() => {
            console.log('延时3秒');
        },3000)
    

    很明显 3秒后 打印出数据 再看一个函数

        setTimeout(() => {
            task();=>随意的一个函数
        },3000)
        console.log('执行console');
    

    有时候我们通过定时器来触发函数的时候有没有发现,有时候明明写的延时3秒,实际却5,6秒才执行函数,当然这个函数需要一定的复杂性,一时间难以写出就 顺带说明一下

    我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

    关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。也就说哪怕定时器之前没有任何事件,也要4毫秒之后才运行setTimeout 有兴趣的同学可以自行了解

    接着我们看下promise(微任务)

        console.log('start')   第一
        let p = new Promise((resolve,reject)=>{
        console.log('Promise1')  第二
        resolve()
        })
        p.then(()=>{
        console.log('Promise2')     第四
        })
        console.log('end')第三
    

    有些小伙伴又懵了,promise不是异步操作吗
    我们先抛开任务队列不讲,注意了,只有resolve与reject函数 才是真正的异步操作,也就是说在

     new Promise(){
         这一块还是同步事件
     }
    

    因为js是单线程 所以此时 从上到下 先完成同步,再执行异步

    接着我们来分析promise(微任务)混搭setTimeout(宏任务)

        setTimeout(()=>{
        console.log('setTimeout1')
        },0)
        let p = new Promise((resolve,reject)=>{
        console.log('Promise1')
        resolve()
        })
        p.then(()=>{
        console.log('Promise2')    
        })
    

    最后输出结果是Promise1,Promise2,setTimeout1
    虽然同是异步事件,但微任务优先于宏任务(暂时先这么理解)

    再看一个例子

        Promise.resolve().then(()=>{
        console.log('Promise1')  
        setTimeout(()=>{
            console.log('setTimeout2')
        },0)
        })
    
        setTimeout(()=>{
        console.log('setTimeout1')
        Promise.resolve().then(()=>{
            console.log('Promise2')    
        })
        },0)
    

    这回是嵌套,大家可以看看,最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2
    一步步来分析一下
    第一个输出Promise1 应该没什么问题,因为此时是同步
    第二个输出setTimeout1 有点问题来了,为什么promise先执行,但是输出的是下面的setTimeout呢?

    此时给大家理一下正确的任务队列

    当我们同步事件运行完成的时候,我们先来看看当前的任务队列
    首先是 microtasks(微任务队列),此时是上方的promise=>这个应该好理解
    然后是macrotasks(宏任务队列),此时很明显是下方的setTimeout
    先执行微任务队列中的promise,此时会生成一个新的setTimeout(宏任务)事件
    这时候我们宏任务队列增加了一条那就是setTimeout2,那么此时的顺序就是setTimeout1,setTimeout2(还是不懂的同学可以写个记事本记录一下)
    注意,微任务此时已经清空了,对吧。因为promise执行完了
    此时执行宏任务事件,根据顺序先执行setTimeout1,所以第二个打印setTimeout1,照理说接下去打印setTimeout2,但是再执行setTimeout1的时候这个任务又创建了promise 微任务,此时微任务队列又增加了一条primise2事件
    那么老样子,微任务事件一旦生成,优先于宏任务,那么此时又执行了promise2,最后执行setTimeout2

    其实是应该当微任务队列全执行完了才执行宏任务队列,因为此处例子不是很复杂,起初只有一条微任务队列

    这是浏览器中常见的事件,放更复杂的怕大家怀疑人生,希望大家好好理解一下

    接下来我找几道闭包的笔试题来巩固一下什么是闭包

        for (var i = 1; i <= 5; i++) {
    
        setTimeout( function timer() {
    
            console.log(i);
    
        }, 0); 这里输出5个6我想很容易理解
        }
    

    好 不修改代码,如何输出1,2,3,4,5,考验了你对闭包的理解与写法

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

    首先要了解结构,函数套函数
    其次明白闭包的作用,它是获取函数内部的变量,那肯定不要写存在变量函数的里面,不然我们无法获取
    接下来就是如上方所示

    永远不要小瞧代码 下面这道题让新手做 头皮发麻

        function fun(n,o){
        console.log(o);
        return {
            fun:function(m){
                return fun(m,n);
            }
        };
    }
     var a = fun(0);a.fun(1);  a.fun(2);  a.fun(3);
     var b = fun(0).fun(1).fun(2).fun(3);
     var c = fun(0).fun(1);  c.fun(2);c.fun(3);
    

    问上面各个函数打印什么

    解析这道题之前,先带个小坑,如果我们的函数参数未传参,默认为undefined
    也就是说 function fn(a,b){console.log(a,b)}
    那我们使用的时候fn(b) 那么a其实是等于undefined的

    好接来下我们来一步步解析一下这道题

    一部部来 先看var a 一共四个值 fun(0);a.fun(1);a.fun(2);a.fun(3);
    先解释fun(0) 我们此时将代码带入 ,注意此时函数返回的是一个对象

            {
            fun:function(m){
                return fun(m,n);
            }
    

    到这里就结束了,刚才提过未带入参数,则默认undefined 那么此时 fun(0)最后打印结果就是undefined
    接下来是a.fun(1),注意此时a已经改变了,var a=fun(0) a已经默认返回了一个对象,此时调用对象里面的fun函数
    仔细看细节,我们给他转换一下就是

    function(1){
     return  fun(1,n) =>注意这里 他调用了上面那个函数 但是这里传入了两个函数 
    }
    

    到这里是不是有思路了呢,我把函数整理一下 再看一遍

        function fun(n,o){                第一步   首先我们进行fun(0)
        console.log(o);                    也就是说此时里面的值其实是这样的,n=0,o=undefined(这也是第一步的值)
        return {                               第二步再次进行调用fun,但是注意,此时的作用域变了
            fun:function(m){              function(1)
                return fun(m,n);          此时m=1,但是n等于多少呢,n并没有传入,好 它会去父作用域去找,会找到n=0
            }                                      接着调用最初的fun(n,o)这个方法 ,但此时,n形参对应m实参,o形参对应n实参
        };                                          那么此时o=n=0
    }                                               所以 控制台 将 打印 0  有没有发现其实我们只要判断n的值就可以了
    

    a.fun(2); a.fun(3);的结果其实跟a.fun(1)相同 都是0 不要被迷惑了哦 要相信自己

    来分析一下 b ; var b = fun(0).fun(1).fun(2).fun(3);

    注意这里全是调用,我们一步一步看

    根据第一步的分析我们已经了解了,只需要判断n的值就可以了

    fun(0) 得 n=undefined fun(0).fun(1)得 n=0 按照原来的思路 一直发现 n其实很简单 最终n=2
    四次函数调用 控制台将会打印 undefined 0 1 2 现在是不是觉得很简单了呢

    第三个 希望小伙伴自己分析一下

    相关文章

      网友评论

          本文标题:js闭包 与事件队列

          本文链接:https://www.haomeiwen.com/subject/bleixqtx.html