js基礎梳理-如何理解作用域和作用域鏈?

来源:https://www.cnblogs.com/hezhi/archive/2018/12/09/10090151.html
-Advertisement-
Play Games

本文重點是要梳理執行上下文的生命周期中的建立作用域鏈,在此之前,先回顧下關於作用域的一些知識。 1.什麼是作用域(scope)? 在《JavaScritp高級程式設計》中並沒有找到確切的關於作用域的定義,只是在“4.2執行環境及作用域”中簡單說了下執行環境(execution context)的概念 ...


本文重點是要梳理執行上下文的生命周期中的建立作用域鏈,在此之前,先回顧下關於作用域的一些知識。

1.什麼是作用域(scope)?

在《JavaScritp高級程式設計》中並沒有找到確切的關於作用域的定義,只是在“4.2執行環境及作用域”中簡單說了下執行環境(execution context)的概念。而執行環境其實就是之前博客:js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?中的執行上下文。

而在《JavaScript權威指南》中,對作用域的描述為:

變數作用域:一個變數的作用域(scope)是程式源代碼中定義這個變數的區域

在《你不知道的Javascript·上捲》中對作用域的描述則為:

負責收集並維護由所有生命的標識符(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問許可權。

簡單來講,作用域(scope)就是變數訪問規則的有效範圍

  • 作用域外,無法引用作用域內的變數;
  • 離開作用域後,作用域的變數的記憶體空間會被清除,比如執行完函數或者關閉瀏覽器
  • 作用域與執行上下文是完全不同的兩個概念。我曾經也混淆過他們,但是一定要仔細區分。

JavaScript代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。

說得很深奧的樣子,其實上面這段話重點用函數作用域與函數執行上下文來區分是最好不過的了。函數作用域是在函數聲明的時候就已經確定了,而函數執行上下文是在函數調用時創建的。假如一個函數被調用多次,那麼它就會創建多個函數執行上下文,但是函數作用域顯然不會跟著函數被調用的次數而發生什麼變化。

1.1 全局作用域

var foo = 'foo';
console.log(window.foo);   // => 'foo' 

在瀏覽器環境中聲明變數,該變數會預設成為window對象下的屬性。

function foo() {
    name = "bar"
}
foo();
console.log(window.name) // bar

在函數中,如果不加 var 聲明一個變數,那麼這個變數會預設被聲明為全局變數,如果是嚴格模式,則會報錯。

全局變數會造成命名污染,如果在多處對同一個全局變數進行操作,那麼久會覆蓋全局變數的定義。同時全局變數數量過多,非常不方便管理。

這也是為什麼jquery要在全局建立變數 $,其餘私有方法屬性掛在 $ 下的原因。

1.2 函數作用域

假如在函數中定義一個局部變數,那麼該變數只可以在該函數作用域中被訪問。

function doSomething () {
    var thing = '吃早餐';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined

嵌套函數作用域:

function outer () {
    var thing = '吃早餐';
    function inner () {
        console.log(thing);
    }
    inner();
}

outer();  // 吃早餐

在外層函數中,嵌套一個內層函數,那麼這個內層函數可以向上訪問到外層函數中的變數。

既然內層函數可以訪問到外層函數的變數,那如果把內層函數return出來會怎樣?

function outer () {
    var thing = '吃早餐';
    
    function inner () {
        console.log(thing);
    }
    
    return inner;
}

var foo = outer();
foo();  // 吃早餐

前面提到,函數執行完後,函數作用域的變數就會被垃圾回收。而這段代碼看出當返回了一個訪問了外部函數變數的內部函數,最後外部函數的變數得以保存。

這種當變數存在的函數已經執行結束,但扔可以再次被訪問到的方式就是“閉包”。後期會繼續對閉包進行梳理。

1.3 塊級作用域

很多書上都有一句話,javascript沒有塊級作用域的概念。所謂塊級作用域,就是{}包裹的區域。但是在ES6出來以後,這句話並不那麼正確了。因為可以用 let 或者 const 聲明一個塊級作用域的變數或常量。

比如:

for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

發現這個例子就會和函數作用域中的第一個例子一樣的錯誤提示。因為變數i只可以在 for迴圈的{ }塊級作用域中被訪問了。

擴散思考:

究竟什麼時候該用let?什麼時候該用const?

預設使用 const,只有當確實需要改變變數的值的時候才使用let。因為大部分的變數的值在初始化之後不應再改變,而預料之外的變數的修改是很多bug的源頭。

1.4 詞法作用域

詞法作用域,也可以叫做靜態作用域。意思是無論函數在哪裡調用,詞法作用域都只在由函數被聲明時所處的位置決定。
既然有靜態作用域,那麼也有動態作用域。
而動態作用域的作用域則是由函數被調用時執行的位置所決定。

var a = 123;
function fn1 () {
    console.log(a);
}
function fn2 () {
    var a = 456;
    fn1();
}
fn2();   // 123

以上代碼,最後輸出結果 a 的值,來自於 fn1 聲明時所在位置訪問到的 a 值 123。
所以JS的作用域是靜態作用域,也叫詞法作用域。

上面的1.1-1.3可以看做作用域的類型。而這一小節,其實跟上面三小節還是有差別的,並不屬於作用域的類型,只是關於作用域的一個補充說明吧。

2. 什麼是作用域鏈(scope chain)

在JS引擎中,通過標識符查找標識符的值,會從當前作用域向上查找,直到作用域找到第一個匹配的標識符位置。就是JS的作用域鏈。

var a = 1;
function fn1 () {
    var a = 2;
    function fn2 () {
        var a = 3;
        console.log(a);
    }
    fn2 ();
}
fn1(); // 3

console.log(a) 語句中,JS在查找 a變數標識符的值的時候,會從 fn2 內部向外部函數查找變數聲明,它發現fn2內部就已經有了a變數,那麼它就不會繼續查找了。那麼最終結果也就會列印3了。

3. 作用域鏈與執行上下文

在此前的博客:js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?中講到執行上下文的生命周期:

3.執行上下文的生命周期

3.1 創建階段

  • 生成變數對象(Variable object, VO)
  • 建立作用域鏈(Scope chain)
  • 確定this指向

3.2 執行階段

  • 變數賦值
  • 函數引用
  • 執行其他代碼

上面做了那麼多鋪墊,其實重點是想梳理這一小節。
下麵,以一個函數的創建和激活兩個時期來講解作用域鏈是如何創建及變化的。

3.1函數創建階段

上文中講到,函數的作用域在函數定義的時候就決定了。

這是因為函數有一個內部屬性[[scope]],當函數創建的時候,就會保存所有父變數對象到其中,但是註意:此時[[scope]]並不代表完整的作用域鏈,因為在創建階段,它還沒有包括自己的作用域。

舉個慄子:

function foo () {
    function bar () {
        ...
    }
}

函數創建時,各自的[[scope]]為:

foo.[[scope]] = [
    globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.AO
];

3.2 函數激活階段

當函數激活時,進入函數上下文,創建VO/AO後,就會將活動對象添加到作用域鏈的前端。

這時候執行上下文的作用域鏈,命名為 Scope:

Scope = [AO].concat([[scope]]);

至此,作用域鏈創建完畢。

3.3 舉個慄子

以下麵的例子為例,結合之前的變數對象,活動對象和執行上下文棧,總結一下函數執行上下文中作用域鏈和變數對象的創建過程:

var x = 10;
 
function foo() {
  var y = 20;
 
  function bar() {
    var z = 30;
    console.log(x +  y + z);
  }
 
  bar();
}
 
foo(); // 60

大家肯定都知道列印結果會是60。但是從第一行代碼開始到最後一行代碼結束,整個代碼的執行上下文棧以及作用域鏈是怎樣變化的呢?

// 第一步:進入全局上下文,此時的執行上下文棧是這樣:
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
];

// 第二步:foo函數被創建,此時的執行上下文棧沒有變化,但是創建了foo函數的作用域,保存作用域鏈到內部屬性[[scope]]。
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
];
foo.[[scope]] = [
    globalContext.VO
];

// 第三步:foo函數執行,進入foo函數執行上下文的創建階段
// 這個階段它做了三件事:
// 1.複製之前的foo.[[scope]]屬性到foo函數上下文下,創建foo函數的作用域鏈;
// 2. 創建foo函數上下文的變數對象,並初始化變數對象,依次加入形參,函數聲明,變數聲明
// 3. 把foo函數上下文的變數對象加入到第一步創建的foo函數作用域鏈的最前面。
// 最終,經過這三個步驟之後,整個執行上下文棧是這樣

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        VO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: undefined
        },
        Scope: [foo.VO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.VO,
    globalContext.VO
];

// 第四步:foo函數執行,進入foo函數執行上下文的執行階段。
// 這個階段又做了以下2件事:
// 1. 把foo執行上下文的變數對象VO改成了活動對象AO,並且修改AO中變數的值
// 2. 發現創建了一個 bar函數,就保存了bar函數的所有父變數對象到bar函數的[[scope]]屬性上。


ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    foo.AO,
    globalContext.VO
];

// 第五步,bar函數執行,進入bar函數執行上下文的創建階段
// 與第三步類似,也做了三件事,只不過主體變成了bar
// 1.複製之前的bar.[[scope]]屬性到bar函數上下文下,創建foo函數的作用域鏈;
// 2. 創建bar函數上下文的變數對象,並初始化變數對象,依次加入形參,函數聲明,變數聲明
// 3. 把bar函數上下文的變數對象加入到第一步創建的bar函數作用域鏈的最前面。
// 最終,經過這三個步驟之後,整個執行上下文棧是這樣

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    },
    <bar>functionContext: {
        VO: {
            arguments: {
                length: 0
            },
            z: undefined
        },
        Scope: [bar.VO, foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    bar.VO,
    foo.AO,
    globalContext.VO
];

// 第六步:bar函數執行,進入bar函數執行上下文的執行階段
// 與第四步類似。不過此時bar函數裡面不會再創建新的函數上下文了
// 1. 把bar執行上下文的變數對象VO改成了活動對象AO,並且修改AO中變數的值
ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    },
    <foo>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            bar: <reference to function bar() {}>,
            y: 20
        },
        Scope: [foo.AO, globalContext.VO]
    },
    <bar>functionContext: {
        AO: {
            arguments: {
                length: 0
            },
            z: 30
        },
        Scope: [bar.AO, foo.AO, globalContext.VO]
    }
];

foo.[[scope]] = [
    foo.AO,
    globalContext.VO
];

bar.[[scope]] = [
    bar.AO,
    foo.AO,
    globalContext.VO
];

// 第七步:執行bar函數中的console.log(x + y +z),查找x, y, z三個標識符

- "x"
-- <bar>functionContext.AO   // 沒找到,繼續到foo.AO中找
-- <foo>functionContext.AO   // 還沒找到,再往globalContext.VO中找
-- globalContext.VO     // 找到了,值為 10

- "y"
-- <bar>functionContext.AO   // 沒找到,繼續到foo.AO中找
-- <foo>functionContext.AO   // 找到了,值為20

-- "z"
-- <bar>functionContext.AO   // 找到了,值為 30

列印結果: 60。

// 第八步:bar函數執行完畢,將其從執行上下文棧中彈出,foo函數執行完畢,將其從執行上下文棧中彈出。最終,執行上下文棧,只剩下globalContext

ECStack = [
    globalContext: {
        VO: {
            foo: <reference to function foo() {}>,
            x: 10
        }
    }
]

感覺其實可以簡化理解一下,把第三四步,第五六步分別分成一個步驟。

打算每周定一個小主題,可能是基礎知識鞏固,也可能是自己學習新知識的記錄。在下一篇博文中,將對this指向問題進行梳理。如果你也感興趣,也可以去搜集下相關資料,到時候大家共同學習探討一下。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 12.2 設置LOCAL_TEMP_TABLESPACESQL> select username,DEFAULT_TABLESPACE,TEMPORARY_TABLESPACE,LOCAL_TEMP_TABLESPACE from dba_users;USERNAME DEFAULT_TABLESP ...
  • oracle的分頁查詢詳細分析以及oracle的常用組合函數溫習 ...
  • 在我們使用資料庫進行查詢或者建表時,經常需要查看表結構,下麵以employees資料庫中的departments表為例進行表結構查詢: 以上三種方法的查詢結果相同: 方法 4: 借用MySQL自身的information_schema資料庫,輸入如下指令: 查詢結果如下: 建表信息查詢 : show ...
  • 因為項目不同,有些公用庫而且還是c++的,還有一些帶資源的,簡單的複製遇到庫升級又是一輪配置,編譯成aar則解決這些麻煩。 但是預設andriod studio的make moudle只生成debug包,在3.0以上及時生成release的apk,release的aar也不會生成。 這個時候要用到 ...
  • 1.某個控制項要放在Linearlayout佈局的底部(底部導航條) ...//嵌套的其他佈局…… ...//嵌套的其他佈局 簡單說明一下,上面的代碼中有一個Linearlayout,裡面嵌套了兩個Linearlayout 這裡的關鍵是嵌套裡面的第一個 佈局,註意這個佈局裡面的這兩行屬性代碼 第二個L ...
  • 從零學習Fluter(四):Flutter中ListView組件系列詳展 ...
  • 上一篇基於修改系統源碼的前提下,實現了完全的沉浸式體驗效果。可參考這篇 [戳這][1] 一、自定義Dialog 在沉浸式效果下,當界面彈出對話框時,對話框將獲取到焦點,這將導致界面退出沉浸式效果,那麼是不是能通過屏蔽對話框獲取焦點來達到不退出沉浸式的目的呢。說乾就乾,我們先來看一下改善後的效果圖。 ...
  • 這兩天看了javascript DOM編程。 對象:由一些屬性和方法組合在一起而構成的一個數據實體。 屬性是隸屬於某個特定對象的變數。 方法是只有某個特定對象才能調用的函數。 對象分為: 內建對象:比如Array() 數組對象;Math對象Date對象。 宿主對象:Form對象 Image對象 El ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...