前端入門17-JavaScript進階之作用域

来源:https://www.cnblogs.com/dasusu/archive/2018/12/05/10073633.html
-Advertisement-
Play Games

聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...


聲明

本系列文章內容全部梳理自以下幾個來源:

作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。

PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。

正文-作用域

在 ES5 中,變數的作用域只有兩類:

  • 全局作用域

  • 函數作用域

只要不是在函數內部定義的變數,作用域都是全局的,全局的變數在哪裡都可以被訪問到,即使跨 js 文件。

函數作用域是指在函數體定義的變數,不管有沒有在函數體的開頭定義,在函數體的任何地方都可以被使用,因為 JavaScript 中的變數有聲明提前的行為。

函數作用域需要區別於 Java 語言中的塊級作用域:

var i = 0;
function A() {
    console.log(i); //輸出undefined
    for (var i = 0; i < 1; i++) {}
    console.log(i); //輸出1
}

在 Java 中,類似的代碼,在 for 迴圈前後輸出的 i 都會是 0,因為都會使用成員變數 i,for迴圈內定義的 i 由於塊級作用域限制,只在for 迴圈的 {} 大括弧中的代碼有效。

但在 JavaScript 中,變數作用域只分函數作用域,而且變數有聲明提前的特性,所以在函數體內部第一次輸出 i 時,此時變數已經提前聲明,但還沒初始化,所以會是 undefined。而函數內定義的變數的作用域或者說生命周期是整個函數內,所以即使 for 迴圈體語句結束,仍舊可以訪問到 i 變數。

由於允許變數的重覆定義,所以全局變數很容易起衝突,因為無法確保多份 js 文件中是否已經在全局中定義了該變數,一旦起衝突,瀏覽器行為僅僅是將後定義的覆蓋掉前定義的而已,這對於瀏覽器角度沒什麼大問題,但對於程式而已,很容易出現不可控的問題。而且,極難排查。

所以,實際編程中,建議不要過多的使用全局變數,有多種方法可以避免:

  • 使用一個全局對象來作為命名空間,將其餘不在函數體內部定義的變數,作為該全局對象的屬性來定義使用。
  • 使用一個立即執行的函數來作為臨時命名空間,函數執行結束釋放臨時命名空間。
  • 如果臨時命名空間內的部分變數需要供外部使用,一可以將這部分變數添加到作為命名空間的全局對象上的屬性,二可以利用閉包的特性,返回一個新建的對象,為該對象添加一些介面可訪問這部分變數。

全局對象作為命名空間

var DASU = {};
DASU.num = 1;
function a() {
    console.log(DASU.num);
}

這裡的全局對象意思是說,數據類型為對象的全局變數,簡稱全局對象,與前端里說的全局對象window是兩個不同概念,區分一下。

其實也就是一種思想,將所有函數外需要定義的變數,都替換成對指定對象的屬性來操作。

立即執行的函數作為臨時命名空間

(function () {
    var num = 1;
    function a() {
        console.log(num);
    }
    a();
}())

當引入 js 文件到 HTML 時,js 文件中的代碼就會被執行,或者聲明瞭 <script> 標簽後,在標簽內的代碼也會立馬被執行。但函數只有被調用的時候才會執行,所以,如果我們使用一個立即執行的函數,那這個函數體內部的代碼行為就跟正常的 js 文件代碼被執行的行為一致了。

而且,還可以利用函數內作用域這一特點,來保證,在這個立即執行的函數內部定義的變數不會影響到全局變數。

缺點就是函數內部代碼執行結束後,這些在函數內定義的變數就被回收了。所以,如果有些信息需要跨 js 文件通信,此時要麼通過全局對象方式,要麼通過閉包特性來輔助實現。

臨時命名空間內的變數共用方式

全局變數可以在任何地方被訪問,所以可以將那些需要共用給外部使用的臨時命名空間內的變數賦值給全局對象的屬性,即結合第一種:全局對象做命名空間方式。

或者,通過閉包的特性,作為臨時命名空間的立即執行的函數需要有一個返回值,當外部持有這個返回值時,這個函數內的變數就不會被回收。

然後,返回值可以是一個對象,公開一些介面來獲取這些需要共用的變數,如:

var model = (function () {
    var num = 1;
    function a() {
        console.log(num);
    }
    return {
        getNum: function () {
            return num;
        }
    }
}());
model.getNum();

//或者:
var model = (function () {
    var num = 1;
    function a() {
        console.log(num);
    }
    return {
        num:num
    }
}());
model.num;

變數的聲明提前原理

看個例子:

var i = 0;
function A() {
    console.log(i); //輸出undefined
    for (var i = 0; i < 1; i++) {}
    console.log(i); //輸出1
}
A();

函數內第一個輸出 undefined 是因為變數的聲明提前,第二個輸出 1 是因為變數作用域為函數作用域,而不是塊級作用域。

那麼,有想過,這些似乎理所當然的基礎常識原理是什麼嗎?

我們先來看些理論,再結合理論返回來分析這個例子,但只分析變數的聲明提前原理,至於作用域的原理留著作用域鏈一節分析。

理論

我們之前有介紹過執行上下文 EC,和變數對象 VO,執行上下文分全局執行上下文和函數執行上下文。在全局執行上下文中,VO 的具體表現是全局對象;在函數執行上下文中,VO 的具體表現是 AO,AO 存儲著函數內的變數:形參、局部變數、函數自身引用、this、arguments。

不管是執行函數代碼還是全局代碼,js 解釋器會分兩個過程,有的文章翻譯成:進入執行上下文階段、執行代碼階段(我不怎麼喜歡這個翻譯)。

進入執行上下文階段:其實本質上就是創建一個執行上下文,這個階段會解析當前上下文內的代碼,將聲明的變數都保存到 VO 對象上。

執行代碼階段:就是代碼實際運行期,當運行到相對應的變數的賦值語句時,就會將具體的屬性值寫入 VO 對象上保存的對應變數。

也就是說,在執行代碼階段,代碼實際運行時,js 解釋器已經解析了一遍上下文內的代碼,並創建了執行上下文,且為其添加了一個 VO 屬性,在 VO 對象上添加了上下文內聲明的所有變數,這就是變數的聲明提前行為。而之後函數體內對各變數的操作,其實是對 VO 上保存的變數進行操作了。

我看過一篇文章對這兩個過程的翻譯是:解析階段、執行階段。

我比較喜歡這種翻譯,解析階段主要的工作就是解析上下文內的代碼,創建執行上下文,創建變數對象 VO 等,為執行階段做準備;而執行階段就是代碼實際運行過程。

分析

var i = 0; 
function A() {
    console.log(i); //輸出undefined
    for (var i = 0; i < 1; i++) {}
    console.log(i); //輸出1
}
A();

再回過頭來看這個簡單的例子,假設這段代碼放在一份單獨的 js 文件中,解釋器第一次執行這份代碼,那麼當執行全局代碼時,首先進入全局執行上下文的解析階段:

  1. 解析代碼創建全局執行上下文
  2. 創建VO,併為其添加屬性 i、A
  3. 省略該過程其他工作
  4. 將創建的全局EC放入ECS棧內

當實際開始執行第一行全局代碼時,js解釋器經過瞭解析階段已經做瞭如上的工作,得到了一些基本的信息。之後便是執行全局代碼,如果執行的代碼是訪問全局變數,那麼直接讀取全局 EC 中 VO 里的對應變數;如果是對全局變數賦值操作,那麼寫入全局 EC 中的 VO 里對應變數的屬性值。

如果執行的代碼是調用某個函數,此時就會為這個函數的執行創建一個函數執行上下文,那麼這個過程同樣需要兩個階段:解析階段和執行階段。

所以當代碼執行到最後一行 A() 時,此時新的函數執行上下文的解析階段做的工作:

  1. 解析 A() 函數內代碼,並創建函數執行上下文 A函數EC
  2. 創建 AO,併為其添加屬性
  3. 省略其他工作介紹
  4. 將創建的A函數EC放入ECS棧內

所以當執行函數 A 內的代碼時,第一行輸出才會輸出 undefined,因為變數的聲明提前特性在調用函數時創建函數執行上下文的過程中,已經解析了函數內的聲明語句,並將這些變數添加到函數上下文 EC 的 AO 中了。

AO 就是變數對象 VO 在函數執行上下文中的具體表現。

而當執行完 for 迴圈語句,A 函數 EC 中的 AO 里的i屬性已經被賦值為 1 了,而 A 函數 EC 是直到函數執行結束才銷毀,所以即使在 for 語句內定義的 i 變數也可以在後面繼續使用。

以上,就是變數聲明提前的原理,當然,創建執行上下文的過程中,還涉及到其他很多工作,用來實現例如作用域鏈等機制,留待後續來說。


大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~
dasuAndroidTv2.png


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

-Advertisement-
Play Games
更多相關文章
  • 1.寫一個深度克隆方法(es5)? 2. es6中let,const,var的區別是什麼? 3. 對數組[1,2,3,8,2,8]進行去重,es5或者es6方法? 4. 說說對es6中=>的理解? 5. 點擊一個按鈕,發出ajax請求,如何防止用戶在此請求方式返回之前再次點擊? ...
  • 參考代碼可見: "https://github.com/dashnowords/blogs/tree/master/Structure/GreedyAlogrithm" 一.貪心演算法 屬於比較簡單的演算法,它總是會選擇當下最優解,而不去考慮單次遞歸時是否會對未來造成影響,也就是說不考慮得到的解是否是 ...
  • 基本語法: 1、雙向數據綁定 vue react angular --ngMel(屬性型指令) 2、條件渲染: vue react angular *ngIf(結構型指令) 3、列表渲染: vue react angular angular小案例:Todos ...
  • 貪吃蛇 對不起,您的瀏覽器不支持canvas ...
  • 一、Object.creat()使用方法 Object.creat(對象); 功能:實現繼承,創建一個原型繼承自參數的對象。 什麼是原型式繼承:就是利用修改原型鏈的結構(增加一個節點中的成員,刪除一個節點中的成員,修改一個節點中的成員),來使得實例化對象可以使用整條鏈中的所有成員。 相容方式: fu ...
  • 一、需要實現的效果 這裡使用jQuery來實現。需要實現的效果如下:當下拉條改變時,單選框選中的值隨之變化。 二、註意 當設置單選框的checked屬性時,要使用jQuery的prop()方法,不能夠使用jQuery的attr()方法,attr()只適合簡單html元素設置。 jQuery的prop ...
  • CSS盒模型的概念與分類 CSS盒模型就是一個盒子,封裝周圍的HTML元素,它包括內容content、邊框border、內邊距padding、外邊距margin。 CSS盒模型分為標準模型和IE模型; 標準模型和IE模型的區別 標準模型的width是內容content 的寬度; 設置方式: box- ...
  • 項目中基於skyline的瀏覽器插件進行二次開發,基本的業務操作模式基本上是:點擊功能按鈕開啟操作模式,onFrame預選Feature,onLButtonUp選定Feature並執行業務操作,OnRButtonUp結束操作模式等。這裡封裝了這個基本操作模式,避免代碼重覆,便捷功能擴展和統一。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...