js閉包 與事件隊(duì)列

針對閉包相信小伙伴們有很多不同的概念跟理解
何為閉包,從結(jié)構(gòu)上來講,閉包就是函數(shù)套函數(shù),類似遞歸這種函數(shù)調(diào)用函數(shù)本身的也算是閉包;當(dāng)然這是從結(jié)構(gòu)上來看,從閉包的特點(diǎn)來看,遞歸又不算是閉包
閉包的作用主要是獲取函數(shù)內(nèi)部的局部變量,這也是Javascript語言的特殊之處

JS 的閉包包含以下要點(diǎn):

函數(shù)聲明的時(shí)候,會(huì)生成一個(gè)獨(dú)立的作用域
同一作用域的對象可以互相訪問
作用域呈層級(jí)包含狀態(tài),形成作用域鏈,子作用域的對象可以訪問父作用域的對象,反之不能;另外子作用域會(huì)使用最近的父作用域的對象

上代碼理解閉包,通過實(shí)例會(huì)更容易理解

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

js中變量只有兩種情況,局部變量與全局變量
這就是函數(shù)的作用域鏈
我把代碼解構(gòu)一下 方便大家理解

   function f1(){                                ---------------------------------------------

   var n=999;
                                                                                 此處都屬于f1的作用域 也可以稱之父作用域
            return function f2(){   -------------------------

                      console.log(n);                      這段就是子作用域

                        }-----------------------------------------
    
   }--------------------------------------------------------------------------------------

  f1()()// 999

f1()的結(jié)果是返回f2 f2()函數(shù)執(zhí)行 打印n 但是此時(shí)f2中并沒有n
那么他就會(huì)往它的父作用域去找 ,好 此時(shí)找到了n 那么此時(shí)n為999

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

相同的, 如果f1里不存在n 那么就會(huì)再去上一級(jí) 也就是window中尋找n 那么此時(shí)n就是window中的n

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

此時(shí) f1中也有了n 那么 根據(jù)前面說的 閉包的特點(diǎn) 子作用域會(huì)使用最近的父作用域的對象
他會(huì)取到最近的作用域的屬性 此時(shí)n為80

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

注意,作用域只會(huì)向上尋找,不會(huì)向下,函數(shù)的聲明位置是無所謂的,這根函數(shù)的預(yù)解析有關(guān),函數(shù)的聲明只跟調(diào)用有關(guān),此時(shí)在函數(shù)向上尋找n的過程中沒有發(fā)現(xiàn)n,其實(shí)已經(jīng)發(fā)現(xiàn)了,n等于undefined此時(shí),因?yàn)関ar 變量提升

通過這個(gè)例子,我想大家很容易理解上面特點(diǎn)的第三條

作用域呈層級(jí)包含狀態(tài),形成作用域鏈,子作用域的對象可以訪問父作用域的對象,反之不能;另外子作用域會(huì)使用最近的父作用域的對象

那么我們也說過 閉包的作用主要是獲取函數(shù)內(nèi)部的局部變量
我們嘗試來提取一下

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()() 

這樣我們就可以將子函數(shù)的變量給提取出來了

我們都知道 for循環(huán)是異步進(jìn)行的 如果我們在外面調(diào)用此方法,打印出來的是10個(gè)10

待會(huì)我們一起分析幾道閉包的筆試題來做更深入的了解

這里我們番外一下,什么是js的 任務(wù)隊(duì)列

在此之前希望你對promise又簡單的了解,不然下方的例子可能看不懂

首先我們要知道,javascript它是單線程語言

JavaScript語言的一大特點(diǎn)就是單線程,也就是說,同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來很復(fù)雜的同步問題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會(huì)改變。

之所以js是單線程語言

就是說在一行代碼執(zhí)行的過程中,必然不會(huì)存在同時(shí)執(zhí)行的另一行代碼,就像使用alert()以后進(jìn)行瘋狂console.log,如果沒有關(guān)閉彈框,控制臺(tái)是不會(huì)顯示出一條log信息的

亦或者有些代碼執(zhí)行了大量計(jì)算,比方說在前端暴力破解密碼之類的鬼操作,這就會(huì)導(dǎo)致后續(xù)代碼一直在等待,頁面處于假死狀態(tài),因?yàn)榍斑叺拇a并沒有執(zhí)行完。

所以如果全部代碼都是同步執(zhí)行的,這會(huì)引發(fā)很嚴(yán)重的問題,比方說我們要從遠(yuǎn)端獲取一些數(shù)據(jù),難道要一直循環(huán)代碼去判斷是否拿到了返回結(jié)果么?就像去飯店點(diǎn)餐,肯定不能說點(diǎn)完了以后就去后廚催著人炒菜的,會(huì)被揍的。

于是就有了異步事件的概念,注冊一個(gè)回調(diào)函數(shù),比如說發(fā)一個(gè)網(wǎng)絡(luò)請求,我們告訴主程序等到接收到數(shù)據(jù)后通知我,然后我們就可以去做其他的事情了。

然后在異步完成后,會(huì)通知到我們,但是此時(shí)可能程序正在做其他的事情,所以即使異步完成了也需要在一旁等待,等到程序空閑下來才有時(shí)間去看哪些異步已經(jīng)完成了,可以去執(zhí)行。

比如說打了個(gè)車,如果司機(jī)先到了,但是你手頭還有點(diǎn)兒事情要處理,這時(shí)司機(jī)是不可能自己先開著車走的,一定要等到你處理完事情上了車才能走。

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

因?yàn)橥教美斫饬?,從上向下?zhí)行操作,主要講一下異步事件

微任務(wù)(microtask)與宏任務(wù)(macrotask)=>異步事件

mircotask(微任務(wù))

promise
mutation.oberver
process.nextTick

marcotask(宏任務(wù))

setTimeout,setInterval
requestAnimationFrame
解析HTML
執(zhí)行主線程js代碼
修改url
頁面加載
用戶交互

有些沒見過或者不認(rèn)識(shí)的沒關(guān)系,因?yàn)橛行┦莕ode.js中的 我這邊也主要舉例瀏覽器內(nèi)的事件隊(duì)列

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

從這里我們可以看出 微任務(wù)(mircotask)它的事件比宏任務(wù)(marcotask)優(yōu)先執(zhí)行

解讀:

同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行"場所",同步的進(jìn)入主線程,異步的進(jìn)入Event Table并注冊函數(shù)
當(dāng)指定的事情完成時(shí),Event Table會(huì)將這個(gè)函數(shù)移入Event Queue。
主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去Event Queue讀取對應(yīng)的函數(shù),進(jìn)入主線程執(zhí)行。
上述過程會(huì)不斷重復(fù),也就是常說的Event Loop(事件循環(huán))。

        let data = [];
        $.ajax({
            url:www.javascript.com,
            data:data,
            success:() => {
                console.log('發(fā)送成功!');
            }
        })
        console.log('代碼執(zhí)行結(jié)束');

ajax進(jìn)入Event Table,注冊回調(diào)函數(shù)success。
執(zhí)行console.log('代碼執(zhí)行結(jié)束')。
ajax事件完成,回調(diào)函數(shù)success進(jìn)入Event Queue。
主線程從Event Queue讀取回調(diào)函數(shù)success并執(zhí)行。

微任務(wù)和宏任務(wù)皆為異步任務(wù),也就是說它們都會(huì)進(jìn)入Event Table,它們都屬于一個(gè)隊(duì)列,主要區(qū)別在于他們的執(zhí)行順序,Event Loop的走向和取值。那么他們之間到底有什么區(qū)別呢?

我們先來看看setTimeout(宏任務(wù)),這個(gè)大家應(yīng)該很熟悉

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

很明顯 3秒后 打印出數(shù)據(jù) 再看一個(gè)函數(shù)

    setTimeout(() => {
        task();=>隨意的一個(gè)函數(shù)
    },3000)
    console.log('執(zhí)行console');

有時(shí)候我們通過定時(shí)器來觸發(fā)函數(shù)的時(shí)候有沒有發(fā)現(xiàn),有時(shí)候明明寫的延時(shí)3秒,實(shí)際卻5,6秒才執(zhí)行函數(shù),當(dāng)然這個(gè)函數(shù)需要一定的復(fù)雜性,一時(shí)間難以寫出就 順帶說明一下

我們知道setTimeout這個(gè)函數(shù),是經(jīng)過指定時(shí)間后,把要執(zhí)行的任務(wù)(本例中為task())加入到Event Queue中,又因?yàn)槭菃尉€程任務(wù)要一個(gè)一個(gè)執(zhí)行,如果前面的任務(wù)需要的時(shí)間太久,那么只能等著,導(dǎo)致真正的延遲時(shí)間遠(yuǎn)遠(yuǎn)大于3秒。

關(guān)于setTimeout要補(bǔ)充的是,即便主線程為空,0毫秒實(shí)際上也是達(dá)不到的。根據(jù)HTML的標(biāo)準(zhǔn),最低是4毫秒。也就說哪怕定時(shí)器之前沒有任何事件,也要4毫秒之后才運(yùn)行setTimeout 有興趣的同學(xué)可以自行了解

接著我們看下promise(微任務(wù))

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

有些小伙伴又懵了,promise不是異步操作嗎
我們先拋開任務(wù)隊(duì)列不講,注意了,只有resolve與reject函數(shù) 才是真正的異步操作,也就是說在

 new Promise(){
     這一塊還是同步事件
 }

因?yàn)閖s是單線程 所以此時(shí) 從上到下 先完成同步,再執(zhí)行異步

接著我們來分析promise(微任務(wù))混搭setTimeout(宏任務(wù))

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

最后輸出結(jié)果是Promise1,Promise2,setTimeout1
雖然同是異步事件,但微任務(wù)優(yōu)先于宏任務(wù)(暫時(shí)先這么理解)

再看一個(gè)例子

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

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

這回是嵌套,大家可以看看,最后輸出結(jié)果是Promise1,setTimeout1,Promise2,setTimeout2
一步步來分析一下
第一個(gè)輸出Promise1 應(yīng)該沒什么問題,因?yàn)榇藭r(shí)是同步
第二個(gè)輸出setTimeout1 有點(diǎn)問題來了,為什么promise先執(zhí)行,但是輸出的是下面的setTimeout呢?

此時(shí)給大家理一下正確的任務(wù)隊(duì)列

當(dāng)我們同步事件運(yùn)行完成的時(shí)候,我們先來看看當(dāng)前的任務(wù)隊(duì)列
首先是 microtasks(微任務(wù)隊(duì)列),此時(shí)是上方的promise=>這個(gè)應(yīng)該好理解
然后是macrotasks(宏任務(wù)隊(duì)列),此時(shí)很明顯是下方的setTimeout
先執(zhí)行微任務(wù)隊(duì)列中的promise,此時(shí)會(huì)生成一個(gè)新的setTimeout(宏任務(wù))事件
這時(shí)候我們宏任務(wù)隊(duì)列增加了一條那就是setTimeout2,那么此時(shí)的順序就是setTimeout1,setTimeout2(還是不懂的同學(xué)可以寫個(gè)記事本記錄一下)
注意,微任務(wù)此時(shí)已經(jīng)清空了,對吧。因?yàn)閜romise執(zhí)行完了
此時(shí)執(zhí)行宏任務(wù)事件,根據(jù)順序先執(zhí)行setTimeout1,所以第二個(gè)打印setTimeout1,照理說接下去打印setTimeout2,但是再執(zhí)行setTimeout1的時(shí)候這個(gè)任務(wù)又創(chuàng)建了promise 微任務(wù),此時(shí)微任務(wù)隊(duì)列又增加了一條primise2事件
那么老樣子,微任務(wù)事件一旦生成,優(yōu)先于宏任務(wù),那么此時(shí)又執(zhí)行了promise2,最后執(zhí)行setTimeout2

其實(shí)是應(yīng)該當(dāng)微任務(wù)隊(duì)列全執(zhí)行完了才執(zhí)行宏任務(wù)隊(duì)列,因?yàn)榇颂幚硬皇呛軓?fù)雜,起初只有一條微任務(wù)隊(duì)列

這是瀏覽器中常見的事件,放更復(fù)雜的怕大家懷疑人生,希望大家好好理解一下

接下來我找?guī)椎篱]包的筆試題來鞏固一下什么是閉包

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

    setTimeout( function timer() {

        console.log(i);

    }, 0); 這里輸出5個(gè)6我想很容易理解
    }

好 不修改代碼,如何輸出1,2,3,4,5,考驗(yàn)了你對閉包的理解與寫法

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

        (function(i){

            setTimeout( function timer() {

                  console.log(i);

              },  0 );

        })(i);

    }

首先要了解結(jié)構(gòu),函數(shù)套函數(shù)
其次明白閉包的作用,它是獲取函數(shù)內(nèi)部的變量,那肯定不要寫存在變量函數(shù)的里面,不然我們無法獲取
接下來就是如上方所示

永遠(yuǎn)不要小瞧代碼 下面這道題讓新手做 頭皮發(fā)麻

    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);

問上面各個(gè)函數(shù)打印什么

解析這道題之前,先帶個(gè)小坑,如果我們的函數(shù)參數(shù)未傳參,默認(rèn)為undefined
也就是說 function fn(a,b){console.log(a,b)}
那我們使用的時(shí)候fn(b) 那么a其實(shí)是等于undefined的

好接來下我們來一步步解析一下這道題

一部部來 先看var a 一共四個(gè)值 fun(0);a.fun(1);a.fun(2);a.fun(3);
先解釋fun(0) 我們此時(shí)將代碼帶入 ,注意此時(shí)函數(shù)返回的是一個(gè)對象

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

到這里就結(jié)束了,剛才提過未帶入?yún)?shù),則默認(rèn)undefined 那么此時(shí) fun(0)最后打印結(jié)果就是undefined
接下來是a.fun(1),注意此時(shí)a已經(jīng)改變了,var a=fun(0) a已經(jīng)默認(rèn)返回了一個(gè)對象,此時(shí)調(diào)用對象里面的fun函數(shù)
仔細(xì)看細(xì)節(jié),我們給他轉(zhuǎn)換一下就是

function(1){
 return  fun(1,n) =>注意這里 他調(diào)用了上面那個(gè)函數(shù) 但是這里傳入了兩個(gè)函數(shù) 
}

到這里是不是有思路了呢,我把函數(shù)整理一下 再看一遍

    function fun(n,o){                第一步   首先我們進(jìn)行fun(0)
    console.log(o);                    也就是說此時(shí)里面的值其實(shí)是這樣的,n=0,o=undefined(這也是第一步的值)
    return {                               第二步再次進(jìn)行調(diào)用fun,但是注意,此時(shí)的作用域變了
        fun:function(m){              function(1)
            return fun(m,n);          此時(shí)m=1,但是n等于多少呢,n并沒有傳入,好 它會(huì)去父作用域去找,會(huì)找到n=0
        }                                      接著調(diào)用最初的fun(n,o)這個(gè)方法 ,但此時(shí),n形參對應(yīng)m實(shí)參,o形參對應(yīng)n實(shí)參
    };                                          那么此時(shí)o=n=0
}                                               所以 控制臺(tái) 將 打印 0  有沒有發(fā)現(xiàn)其實(shí)我們只要判斷n的值就可以了

a.fun(2); a.fun(3);的結(jié)果其實(shí)跟a.fun(1)相同 都是0 不要被迷惑了哦 要相信自己

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

注意這里全是調(diào)用,我們一步一步看

根據(jù)第一步的分析我們已經(jīng)了解了,只需要判斷n的值就可以了

fun(0) 得 n=undefined fun(0).fun(1)得 n=0 按照原來的思路 一直發(fā)現(xiàn) n其實(shí)很簡單 最終n=2
四次函數(shù)調(diào)用 控制臺(tái)將會(huì)打印 undefined 0 1 2 現(xiàn)在是不是覺得很簡單了呢

第三個(gè) 希望小伙伴自己分析一下

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容