由兩道題擴充套件的對作用域,作用域鏈,閉包,立即執行函式,匿名函式的認識總結

Kerinlin發表於2019-03-03

前言

最近在學JS,前幾天看到兩道題,剛開始看懵懵懂懂,這幾天通過各種查資料,慢慢的理解,頓悟了,對匿名函式,閉包,立即執行函式的理解也更深了一點,在此分享給大家我的理解與總結,希望能幫助大家理解.因為這篇文章是我用心總結的,查閱了很多的資料,所以總結的比較細,篇幅較長,如果沒耐心,建議跳出,點個收藏,以後如果要用到,有耐心想看時,方便查閱.另外如果有啥錯誤,還望指正


題目一

function fn() {
        for (var i = 0; i < 2; i++) {
            var variate = i;
            setTimeout(function () {
                console.log("setTimeout執行後:" + variate);
            }, 1000);
        }
        console.log(i);
    }
    fn();
複製程式碼

最後結果是啥呢?

由兩道題擴充套件的對作用域,作用域鏈,閉包,立即執行函式,匿名函式的認識總結

結果是,先列印2,再列印2個1
為什麼呢?
先來梳理下函式執行過程:

  • 首先for迴圈遍歷i,(0,1)的時候分別將遍歷值傳給variate變數,variate變數最後儲存的值為1
  • 當i值為2時,指標跳出迴圈,執行到列印i值這步,此時i=2
  • 執行函式fn(),執行完畢後,觸發setTimeout事件,因為迴圈2次,而且最後儲存在這個作用域中變數的值為1,所以最後輸出2個1
  • 所以最後的列印的值為2,1,1

分析完了,先不急,我們先來了解下setTimeout事件

setTimeout事件

  • setTimeout事件有兩個引數:事件,時間開始執行時間
  • setTimeout事件是非同步的
  • 當呼叫setTimeout事件時,會把函式引數,放到事件佇列中。等主程式執行完,再呼叫

理解這個後,答案就很容易得出了


題目二

function fn() {
           for (var i = 0; i < 2; i++) {
               (function () {
                   var variate = i;
                   setTimeout(function () {
                       alert(variate);
                   }, 1000);
               })();
                            
           }
          console.log(i);
          console.log(variate);
       }
       fn(); 
複製程式碼

先分析下整體結構:
函式體內包含一個for迴圈體,迴圈體內又包含一個匿名函式,形成閉包,加上兩個小括號–>(匿名函式)()形成立即執行函式

再思考下函式執行過程

  1. i=0時,進入函式體內,因為是立即執行,所以i值進入匿名函式,通過作用域鏈,變數variate獲得i值,匿名函式體內的setTimeout中的變數variate獲得i值,第一輪迴圈結束;

  2. i=1時,執行與1同樣的過程;

  3. i=2,跳出迴圈,列印i,variate;

結果是啥呢?

由兩道題擴充套件的對作用域,作用域鏈,閉包,立即執行函式,匿名函式的認識總結

Excuse me?竟然有錯誤?

由兩道題擴充套件的對作用域,作用域鏈,閉包,立即執行函式,匿名函式的認識總結

好,那就讓我們來解決錯誤,錯誤顯示variate is not defined,原來是這樣,沒定義,那分析一波,為什麼會顯示未定義呢?
首先我們看函式內部,內部已經定義了,所以我們想到作用域的問題

作用域和作用域鏈

  • 作用域

變數和函式的訪問區域,分全域性作用域函式作用域,在es6中新增let關鍵字後有了塊級作用域概念.

變數提升: JS在解析程式碼前會先將所有函式體內的變數,提升至函式體頂端,來看個例子

var Gscope = "global";
        function t() {
            var Gscope;
            console.log("這是全域性變數:"+Gscope);//這是全域性變數:undefined
            let Lscope = "local";
            console.log("這是區域性變數"+Lscope);//這是區域性變數local
        }  
    t();
    
複製程式碼

為什麼第一個值為undefined?因為函式體內的Gscope變數被提升至函式體頂端,但是未賦值,so,undefined.

let關鍵字:let用於宣告變數,但是let宣告的變數只在let所在的程式碼塊(塊級作用域)有用,OK,show code

for (let i = 0; i < 2; i++) {
            let i = `a`;
            console.log(i);//a a
        }
    console.log(i);//i is not defined
複製程式碼
  • 作用域鏈

什麼是作用域鏈?有什麼用途?怎麼建立起來的?

先引用一句高階程式設計裡的話:

作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件

我的理解是:

作用域鏈就相當於是溝通執行環境內的各個變數與函式的橋樑,通過作用域鏈,同一執行環境裡面的變數和函式都有權利訪問對方;

不同的執行環境間是怎樣的呢?

不同執行環境間的交流還是通過橋樑(作用域鏈),但是現在橋樑變成單行道了,只能允許內部環境訪問外部環境,但外部環境不能訪問內部環境.內部環境通過橋樑能夠向上搜尋查詢變數和函式,但外部卻不能向下搜尋進入另一個執行環境.理解這個後,出現題目二的問題,variate is not defined,就很容易理解了:

因為他們兩個壓根不在同一個執行環境,而且,裡面的變數物件通過閉包能夠訪問外部環境變數,但外部環境變數無權訪問內部的變數variate.

這時可能又蹦出一個問題了,”橋樑”(執行環境的作用域鏈)怎麼搭建起來的呢?

  1. 先建立一個預先包含全域性變數物件的作用域鏈,儲存在內部的[scope]屬性中
  2. 呼叫函式時,為函式搭建一個執行環境
  3. 複製函式的[scope]中的物件構建起執行環境的作用域鏈
  4. 建立活動物件,並將活動物件推入執行環境的前端

分析完後,再重新閱讀下作用域的概念,會發現很有道理!


閉包

首先提出幾個問題:什麼是閉包? 為什麼要用它?它有啥缺點?怎麼建立?

什麼是閉包?

閉包是指有權訪問另一個函式作用域中變數的函式

先貼上剛剛那一段程式碼

function fn() {
           for (var i = 0; i < 2; i++) {
               (function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout執行後:"+variate);
                   }, 1000);
               })();//閉包,立即執行函式,匿名函式
                            
           }
          console.log(i);//2
          console.log(variate);//variate is not defined
       }
       fn(); 

複製程式碼

通過定義可以知道,閉包本質還是作用域鏈的問題.
那為什麼內部環境能訪問外部環境呢?
那就先探討下,函式呼叫時會發生什麼吧!

  1. 先建立執行環境和作用域鏈;
  2. 初始化函式的活動物件(命名引數值,arguments);
  3. 在作用鏈中搜尋具有相應名字的變數,實現對變數的讀取和寫入;
  4. 呼叫執行完畢,銷燬區域性活動物件,僅儲存全域性作用域.
    所以關鍵還是內部函式作用域鏈將外部的活動物件新增到自己作用域中了

這個例子中函式fn()內部巢狀了一個匿名函式形成閉包,內部的variate變數變為私有成員變數,所以外部無法訪問,因而會報錯variate is not defined

為什麼用閉包?

  • 因為在閉包內部保持了對外部活動物件的訪問,但外部的變數卻無法直接訪問內部,避免了全域性汙染;
  • 可以當做私有成員,彌補了因js語法帶來的物件導向程式設計的不足;
  • 可以長久的在記憶體中儲存一個自己想要儲存的變數.

閉包有啥缺點呢?

  1. 可能導致記憶體佔用過多,因為閉包攜帶了自身的函式作用域
  2. 閉包只能取得外部包含函式中得最後一個值

怎麼建立閉包?
在函式內部巢狀使用函式


匿名函式

什麼是匿名函式?
顧名思義,就是沒有名字的函式
如例子中的程式碼就是一個匿名函式

function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout執行後:"+variate);
                   }, 1000);
               }
複製程式碼

匿名函式優缺點?
優點:可以通過var關鍵字建立函式表示式,函式表示式不會出現變數提升的情況,只有在真正被解釋執行的時候才會執行到函式表示式所在的程式碼行,有效避免了全域性汙染;

缺點:匿名函式繫結的事件不能解綁


立即執行函式

什麼是立即執行函式?有什麼作用?

什麼是立即執行函式?

宣告一個匿名函式,並且馬上呼叫它{通過加()的形式}

立即執行函式的形式

(匿名函式)();

 (function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout執行後:"+variate);
                   }, 1000);
               })()
複製程式碼

為什麼要用小括號將匿名函式包裹起來?

為了通過瀏覽器的語法檢查

作用?
建立一個獨立的作用域,避免全域性汙染


小結

通過兩道題擴充套件出來知識點,並且總結出來,現在對知識點的基礎概念,以及一些實現原理有了很清晰的認識,這種感覺很棒

相關文章