javascript之做用域三(理解做用域鏈)

3、做用域鏈

js訪問一個變量時會優先在該做用域內(訪問時的那個做用域)尋找是否聲明過這個變量,若是該變量已經存在,則直接使用它的值,不然會尋找該做用域的‘父做用域/上級做用域',依次類推,直到找到全局做用域爲止。通俗地講,當聲明一個函數時,局部做用域一級一級向上包起來,就是做用域鏈。bash

首先看一個多級做用域的栗子:閉包

//多級做用域
//======>此處是1級做用域
var gender = "男";
function fn(){ // ======>從這裏開始是2級做用域
    //gender 能夠訪問  gender是全局做用域的變量,任何地方均可以訪問
    //age    能夠訪問
    //height 不能訪問

    return function(){ // ======>從這裏開始是3級做用域
        //gender  能夠訪問
        //age     能夠訪問
        //height  能夠訪問
        var height = 180;
    }
    var age = 5;
}
複製代碼

因爲做用域是相對於變量而言的,而若是存在多級做用域,這個變量又來自於哪裏?這個問題就須要好好地探究一下了,咱們把這個變量的查找過程稱之爲變量的做用域鏈函數

做用域鏈的意義:查找變量(肯定變量來自於哪裏,變量是否能夠訪問)post

簡單來講,查找一個變量來自哪裏,可否被訪問,須要如下四步:ui

  1. 查看當前做用域,若是當前做用域聲明瞭這個變量,能夠直接訪問
  2. 查找當前做用域的上級做用域,也就是當前函數的上級函數,看看上級函數中有沒有聲明,有就返回變量,沒有繼續下一步
  3. 再查找上級函數的上級函數,直到全局做用域爲止,有則返回,無則繼續
  4. 若是全局做用域中也沒有,咱們就認爲這個變量未聲明(xxx is not defined)

這四步操做就描述了整個做用域鏈及做用域鏈如何查找變量的過程。spa

  1. 當執行函數時,老是先從函數內部找尋局部變量
  2. 若是內部找不到(函數的局部做用域沒有),則會向建立函數的做用域(聲明函數的做用域)尋找,依次向上

代碼分析:

  1. 當執行fn1時,建立函數fn1的執行環境,並將該對象置於鏈表開頭,
  2. 而後將函數fn的調用對象放在第二位,最後是全局對象,
  3. 做用域鏈的鏈表的結構是fn1->fn->window。
  4. 從鏈表的開頭尋找變量a,即fn1函數內部找變量a,找到了,結果是20。
  5. 一樣,執行fn2時,做用域鏈的鏈表的結構是fn2->fn->window。
  6. 從鏈表的開頭尋找變量a,即fn2函數內部找變量a,找不到,
  7. 因而從fn內部找變量a,找到了,結果是10。
  8. 最後在最外層打印出變量a,直接從變量a的做用域即全局做用域內尋找,結果爲1。

下面咱們來舉幾個特殊的栗子: 例子1:code

function fn(callback){
    var age = 20;
    callback();
}
fn(function(){
    console.log(age);//報錯
    //1.在當前做用域沒有查找到age
    //2.查找上一級做用域:全局做用域
    //爲什麼是全局做用域?
    //由於看上一級做用域,不是看函數在哪調用,而是看函數在哪編寫的。
    //這種特別的做用域,叫作「詞法做用域」
})
複製代碼

這個栗子比較特殊,可能不少人會認爲輸出20,由於函數調用的地方是fn函數內部,恰巧age又是聲明在這個函數內部的,理所應當輸出20。這是錯誤的! 如今分析下做用域鏈如何查找變量的:cdn

  1. console.log(age)的時候,在當前做用域並無查詢到age變量。因此查找上一級做用域。
  2. 上級做用域是誰?這裏須要引出一個概念,查找函數上級做用域,不是看函數在哪調用,而是看函數在哪編寫。因此這樣來看,上級做用域就是全局做用域。
  3. 在全局做用域中並無聲明age變量,因此console.log(age);就會報錯。

例子2:對象

var name="張三";
    function f1(){
        var name = "abc";
        console.log(name);
    }
    f1(); // abc
複製代碼

若是查找一個變量時,在當前做用域找到變量,無論上級、上上級有沒有同名變量都不會再去尋找。blog

例子3:

var name="張三";
    function f1(){
        console.log(name);
        var name = "abc";
    }
    f1(); // undefined
複製代碼

若是這個栗子能看懂說明已經瞭解變量提高,若是不懂,參考後面的文章。

例子4:

var name = "張三";
    function f1(){
        var name = "abc";
        return function(){
            console.log(name);
            console.log(age);
        }
        var age = 18;
    }
    var fn = f1();
    fn();
    //abc 
    //undefined
複製代碼

這個栗子咋一看可能有點懵,可是記住以前一句很重要的話,查找函數上級做用域,不是看函數在哪調用,而是看函數在哪編寫。

例子5:

var name="張三";
    function f1(){
        return {
            say:function(){
                console.log(name);
                var name="abc";
            }
        }
    }
    var fn=f1();
    fn.say();//undefined
複製代碼

當前做用域查到了變量,則不會再繼續尋找,直接返回該變量的值,這裏打印的時候變量聲明可是未賦值,因此輸出undefined。

若是以上幾個栗子都能看懂,說明你已經掌握了做用域&做用域鏈,學好做用域和做用域鏈能夠爲理解閉包打下很好的基礎。

6 閉包

提到做用域就不得不提到閉包,簡單來說,閉包外部函數可以讀取內部函數的變量。

優勢:閉包能夠造成獨立的空間,永久的保存局部變量。

缺點:保存中間值的狀態缺點是容易形成內存泄漏,由於閉包中的局部變量永遠不會被回收

產生閉包的根本緣由是做用域鏈

7 總結

  1. 產生閉包的緣由是由做用域鏈引發的
  2. 函數嵌套函數,被嵌套的函數就能夠稱爲閉包
  3. 子函數可使用父函數的變量(訪問其餘函數內部的局部變量)
  4. 讓變量始終保存在內存中,避免自動垃圾回收(其實上面的例子中就已經用到了的)
  5. 對外提供公有屬性和方法

詳細講解能夠參考我寫的閉包系列:

閉包 juejin.im/post/5d4e40…

相關文章
相關標籤/搜索