js闭包 与事件队列

针对闭包相信小伙伴们有很多不同的概念跟理解
何为闭包,从结构上来讲,闭包就是函数套函数,类似递归这种函数调用函数本身的也算是闭包;当然这是从结构上来看,从闭包的特点来看,递归又不算是闭包
闭包的作用主要是获取函数内部的局部变量,这也是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');    第二个打印
js闭包 与事件队列_第1张图片

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

js闭包 与事件队列_第2张图片

解读:

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入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')第三
js闭包 与事件队列_第3张图片

有些小伙伴又懵了,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闭包 与事件队列)