JS的作用域

文章目錄

  • 一、為什么需要作用域?
  • 二、什么是 JS 作用域?
    • 2.1 什么是詞法作用域和動態作用域?
      • 1. 詞法作用域(Lexical Scpoe)
      • 2. 動態作用域
    • 2.2 JS 的作用域
    • 2.3 JS 作用域的分類
      • 1. 全局作用域
      • 2. 模塊作用域
      • 3. 函數作用域
      • 4. 塊級作用域 (ES6 引入)
      • 5. 對比
    • 2.4 JS 的作用域和詞法作用域的關系
    • 2.5 作用域鏈
      • 1. 理解
      • 2. 它是如何形成的呢?
      • 3. 為什么需要作用域鏈?
      • 4. 總結
  • 三、閉包
      • 1. 什么是閉包?
      • 2. 通過例子理解
      • 3. 總結

一、為什么需要作用域?

想象一下,如果在一個大型 JavaScript 項目中,所有變量都是全局變量會怎樣?我覺得會有下面的問題:

  1. 不同文件/模塊里可能不小心用到同名變量,導致沖突、覆蓋;
  2. 沒有邊界,調試困難;
  3. 開發者需要記憶所有變量名字,增加心智負擔。

所以,我的理解廣泛來說就是限定變量的可見范圍,避免命名沖突,實現數據隔離和封裝,讓代碼更清晰、模塊。

如果從設計者角度去思考的話,我覺得比較重要的原因是:

  1. JS(和大部分編程語言)需要通過作用域,讓名字(標識符)和存儲位置(內存)建立映射關系。
  2. 這樣在編譯或執行時,解釋器/引擎能高效地定位、分配和管理內存。
  3. 同時保證封裝性和可維護性,讓復雜程序拆分成更小、互不干擾的模塊。

所以,我認為作用域讓變量只在需要的地方可見,防止沖突,讓代碼更清晰、可靠。 還能幫助編譯器/解釋器確定變量生命周期和存儲位置的結構化機制。

二、什么是 JS 作用域?

在談 JS 作用域之前,我們先來討論一下詞法作用域和動態作用域。

2.1 什么是詞法作用域和動態作用域?

1. 詞法作用域(Lexical Scpoe)

「Lexical」指的是詞法(lexical analysis):在編譯器前端,對源代碼進行分詞、語法分析的階段。因此詞法作用域也叫靜態作用域(Static Scope):變量作用域在代碼書寫時就確定,不會被運行時的調用關系改變。

大多數現代語言(包括:JavaScript、C、C++、Java、Python、Go、Rust、TypeScript…)都是詞法作用域語言。

關鍵點:

  1. 編譯時就能確定:哪個名字屬于哪個作用域。
  2. 執行時沿著「寫在哪里」形成的作用域鏈找變量。

舉例:

let a = 1;function foo() {console.log(a);
}function bar() {let a = 2;foo();
}bar(); // 輸出 1

foo 定義時在全局,外層有 a = 1。執行時無論 foo 從哪里被調用(bar 調用也好),作用域鏈都不會變:foo 只會在定義處向外找 → 找到全局的 a。

2. 動態作用域

在早期或特殊的語言設計里(如 Lisp 某些方言、Perl 的 local、bash 腳本等),變量的作用域不是在編譯時確定,而是在運行時,根據函數是從哪里被調用決定。所以叫「動態」:變量查找時不是根據寫在哪里,而是看當前的調用棧。

關鍵點:

  1. 調用棧決定作用域。
  2. 執行時如果在當前函數沒找到變量,就在調用者(而非定義時的外層作用域)中找。

還是上述的那個例子: ?? JS 實際不支持動態作用域,但為了說明原理:

let a = 1;function foo() {console.log(a);
}function bar() {let a = 2;foo();
}bar(); // 輸出 2

執行到 bar() 時,bar 調用 foo,foo 查找 a 會先去調用者 bar 的作用域 → 找到 a=2 → 輸出 2。

了解完之后我們再看 JS 的作用域。

2.2 JS 的作用域

首先 JS 的作用域是詞法作用域的一部分,再看 MDN 中對于 JS 作用域的解釋。

根據 MDN 解釋:

  • 作用域是指當前的執行上下文,在其中的值和表達式可以被訪問。

通俗點說:作用域決定了程序的哪些部分可以 “看到” 和 “使用” 某個變量。

舉個例子:

let x = 10
function test() {let y = 20console.log(x) // 可以訪問console.log(y) // 可以訪問
}
test()
console.log(x) // 可以訪問
console.log(y) // 報錯:y is not define

y 只在函數內部可見,外部看不到。

2.3 JS 作用域的分類

JavaScript 中常見的四種作用域:

類型簡介示例
全局作用域腳本模式運行所有代碼的默認作用域var a = 1
模塊作用域模塊模式中運行代碼的作用域export const c = 4
函數作用域由函數創建的作用域function foo() { let x = 2 }
塊級作用域用一對花括號(一個代碼塊)創建出來的作用域{ let b = 3 }

下面我們通過幾個例子,更清楚感受一下各個作用域的實際效果。

1. 全局作用域

在腳本(或 HTML 的 <script>)里直接聲明的變量,就屬于全局作用域,全局可見。

// 全局作用域 
const globalVar = 'I am global'
function sayHello() {console.log(globalVar) // 可以訪問全局變量
}sayHello(); // 輸出:I am global
console.log(globalVar);  // 輸出:I am global

globalVar 定義在最外層(文件最外層或 script 最外層),可以在整個文件或頁面中訪問到。

2. 模塊作用域

當你用 export / import 或 .mjs 模塊時,每個模塊文件默認是私有作用域,文件內部聲明的變量只有本文件能訪問。

// file: utils.js
const secret = 'hidden'
export const publicData = 'exported data'// file: main.js
import { publicData } from './utils.js' 
console.log(publicData) // 輸出:exported data
console.log(secret) // 報錯:secret is not defined

secret 沒有導出,只能在 utils.js 內使用,屬于模塊作用域。publicData 被 export 導出后才能在其他模塊里訪問。

3. 函數作用域

函數內部用var、let、const 定義的變量,只能在這個函數體內部訪問。

function greet() {let name = 'HopeBearer'console.log('Hello, ' + name)
}greet() // 輸出:Hello, HopeBearer
console.log(name) // 報錯:name is not defined

name 只在 greet 函數里可見。函數作用域是最經典的作用域形式。

4. 塊級作用域 (ES6 引入)

在 if、for、while、{} 塊 內用 let 和 const 聲明的變量,只在這個塊內有效。

if(true) {const message = 'inside block'console.log(message) // 輸出:inside block
}
console.log(message) // 報錯:message is not defined

塊級作用域讓你在小范圍內聲明變量,避免污染外層作用域。

注意:var 聲明的變量沒有塊級作用域,只受函數作用域控制。

5. 對比

類型關鍵詞可訪問范圍
全局作用域無特殊關鍵詞全文件 / 頁面
模塊作用域import/export模塊文件內部
函數作用域var let const函數體內部
塊級作用域let const花括號內

2.4 JS 的作用域和詞法作用域的關系

上面我們聊到 JS 的作用域是詞法作用域的一部分,其實指的是: JS的作用域都是詞法作用域體系的一部分。 簡單來說,誰寫在誰里面 -> 形成作用域鏈,執行時,JS 按照 詞法結構(寫在哪里)順序查找變量,不會因為函數是從哪里被調用而改變作用域鏈。

2.5 作用域鏈

1. 理解

作用域鏈(Scope Chain)是 JavaScript 在運行時用來查找變量的一套機制和數據結構。

  • 本質上是一個鏈式結構,由當前執行上下文的變量對象(Variable Environment / Lexical Environment),以及外層(父級)的變量對象,一直串到全局作用域的變量對象。
  • 通過這條鏈,JS 引擎在需要解析變量名時,按順序向外查找直到找到變量,或者到最外層(全局作用域)還沒找到就報錯。

2. 它是如何形成的呢?

作用域鏈不是寫死的,而是根據代碼的詞法結構在函數定義時確定的。具體來說,就是:

當你在寫一個函數時,這個函數「捕獲」了它定義處的外層作用域(也就是詞法作用域)。函數執行時,JS 引擎會根據這個結構把當前作用域對象放到鏈的最前面(頂端),外層作用域依次排在后面。

舉個例子:

let a = 10
function outer() {let b = 20function inner() {let c = 30console.log(a, b, c)}inner()
}
outer()

執行 inner 時的作用域鏈:

[inner 的作用域(包含 c),outer 的作用域(包含 b),全局作用域(包含 a)
]

當 console.log(a, b, c) 執行:

  1. 先在 inner 的作用域里找 a,找不到;
  2. 再去 outer 的作用域找,找不到;
  3. 最后到全局作用域找到 a=10;
  4. 同理,b 在 outer 找到,c 在 inner 找到。

3. 為什么需要作用域鏈?

JS 必須在運行時知道的,一個變量到底屬于那個作用域。

有了作用域鏈:就能:

  1. 保證變量隔離(內外變量不會沖突)
  2. 支持閉包(內部函數能訪問外層變量)
  3. 高效的查找變量(只需從當前開始,逐層向外找)

4. 總結

作用域鏈是 JavaScript 在執行時用來按詞法結構順序查找變量的一條鏈,它讓內部作用域可以訪問到外層作用域的變量,而不是反過來。

三、閉包

都說到這了,我們順便了解一下閉包。

1. 什么是閉包?

MDN 解釋:

  • 閉包是由捆綁起來(封閉的)的函數和函數周圍狀態(詞法環境)的引用組合而成。換言之,閉包讓函數能訪問它的外部作用域。在 JavaScript 中,閉包會隨著函數的創建而同時創建。

我覺得簡單來說就是一個函數“記住了”它被創建時的外層作用域,即使這個函數在外層作用域已經結束后依然可以訪問這些變量。

2. 通過例子理解

舉個例子:

function outer() {let count = 0return function inner() {count++console.log(count)}
}const fn = outer()fn() // 1
fn() // 2

執行流程:

  1. 調用 outer():
    • 創建一個新的作用域 S1。
    • 在 S1 的符號表中記錄:counter -> 內存地址A(初始值0)
  2. outer 內部定義了 inner 函數:
    • inner 的作用域中捕獲了外層作用域 S1。
    • 也就是 inner 會記住:當時 counter 在 S1 的符號表里,對應內存地址A。
  3. outer() 返回 inner 函數:
    • 外層函數 outer 執行結束,理論上作用域 S1 應該銷毀。但是,返回的 inner 函數還引用著 S1 (通過作用域鏈),所以垃圾回收器不會銷毀 S1,也不會釋放內存地址 A。
  4. 后續調用fn:
    • fn 相當于調用 inner
    • inner 會在 S1 中找到 counter,進行自增。
    • 所以每次輸出:1、2、3…

為什么變量不被回收?

我們先看一下 JS 的垃圾回收(GC)的可達性(Reachability):從根出發,只要能沿著引用鏈訪問到的對象,就叫做可達(reachable),不可達(unreachable)對象就認為“沒用了”,可以被垃圾回收。

根(Root)一般是:

  1. 全局對象(比如 window / global
  2. 當前調用棧中的局部變量(活動記錄)
  3. 活動的閉包函數引用的變量

在這個例子中, inner 函數還引用著 外層作用域 S1,S1中的變量 counter就還"活著"。所以可以出現1,2,3…這樣的情況,一旦 fn 被銷毀(比如賦值為 null),S1 就不再被引用,這是垃圾回收期就可以釋放 S1 對應的內存,包括 counter

3. 總結

閉包讓外層作用域中的變量保持活躍, 原因是內部函數把外層作用域放進了自己的作用域鏈里,所以變量依然可訪問,不會被垃圾回收器回收。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91648.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91648.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91648.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OLTP,OLAP,HTAP是什么,數據庫該怎么選

目錄 OLTP&#xff08;Online Transaction Processing&#xff09;聯機事務處理 OLAP&#xff08;Online Analytical Processing&#xff09;聯機分析處理 非實時OLAP 實時OLAP HTAP&#xff08;Hybrid Transactional/Analytical Processing&#xff09; OLAP 和 OLTP 數…

【前端】CSS Flexbox布局示例介紹

CSS Flexbox&#xff08;彈性盒子&#xff09;簡介 Flexbox 是一種一維布局模型&#xff0c;用于高效處理元素在容器內的空間分配、對齊和排序。它通過父容器&#xff08;flex container&#xff09;和子元素&#xff08;flex items&#xff09;的配合實現靈活響應式布局。核心…

Vue3核心語法基礎

一、為什么要學 Composition API&#xff1f;在以前我們寫代碼用Vue2寫&#xff1a;export default {data() {return { count: 0, msg: hello }},methods: {add() { this.count }},computed: {double() { return this.count * 2 }} }很明顯 一個功能被拆成三塊&#xff1a;data…

FSMC的配置和應用

一、FSMC 簡介與工作原理FSMC&#xff08;Flexible Static Memory Controller&#xff09;是 STM32 微控制器中用于與外部靜態存儲器&#xff08;如 SRAM、PSRAM、NOR Flash、LCD 等&#xff09;進行通信的一個外設模塊。1、支持的設備類型&#xff1a;SRAM / PSRAMNOR FlashNA…

Linux I/O 系統調用完整對比分析

Linux I/O 系統調用完整對比分析 1. 概述 Linux 提供了豐富的 I/O 系統調用&#xff0c;每種都有其特定的用途和優勢。本文將詳細分析這些系統調用的特點、使用場景和性能特征。 2. 系統調用詳細對比 2.1 基本讀寫函數 pread/pwrite #include <unistd.h>// 位置指定…

TiDB集群部署

架構&#xff1a; tidb–3臺&#xff0c;pd–3臺&#xff0c;tikv–3臺 8c16g200g 1x2.2x.2x7.124 1x2.2x.2x7.148 1x2.2x.2x7.87 1x2.2x.2x7.93 1x2.2x.2x7.127 1x2.2x.2x7.104 pd-3臺 4c8g100g 1x2.2x.2x7.143 1x2.2x.2x7.132 1x2.2x.2x7.91 1、下載安裝包 #注&#xff1a;我…

C#中對于List的多種排序方式

在 C# 中給 List<AI> 排序&#xff0c;只要 明確排序規則&#xff08;比如按某個字段、某幾個字段、或外部規則&#xff09;&#xff0c;就能用下面幾種常見寫法。下面全部基于這個示例類&#xff1a;public class AI {public int country; // 國家編號public int pr…

Spring框架中Bean的生命周期:源碼解析與最佳實踐

第1章&#xff1a;Spring Bean生命周期概述1.1 什么是Spring Bean生命周期&#xff1f;定義&#xff1a;Spring Bean生命周期是指從Bean的創建、初始化、使用到銷毀的完整過程&#xff0c;由Spring容器嚴格管理 。核心思想是Spring容器通過IoC&#xff08;控制反轉&#xff09;…

【51單片機6位數碼管密碼鎖】2022-10-15

緣由六位密碼器設計連接LED-嵌入式-CSDN問答 矩陣51單片機密碼鎖,回復:https://bbs.csdn.net/topics/392713242_智者知已應修善業的博客-CSDN博客 #include "REG52.h" unsigned char code smgduan[]{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x…

?我的第一個開源項目:躍動的心

還是一個編程初學者時&#xff0c;我懷著激動的心情完成了人生第一個開源項目——一個用HTML5 Canvas制作的動態跳動愛心效果。這個項目雖然簡單&#xff0c;卻讓我深刻體會到了開源分享的快樂和技術創造的魅力。 壹、項目靈感 這個項目的靈感來源于瀏覽網頁時&#xff0c;被各…

技術演進中的開發沉思-53 DELPHI VCL系列:windows的消息(下):TApplication窗體

今天我們梳理下關于TApplication的窗體消息下半部分的內容。前面也說過&#xff0c;在 Delphi 的世界里&#xff0c;TApplication 就像一位經驗豐富的總工程師&#xff0c;而主窗體則是它傾注心血打造的核心建筑。如果你第一次在實驗室里敲出 Delphi 代碼時&#xff0c;屏幕上彈…

cesium FBO(四)自定義相機渲染到Canvas(離屏渲染)

前面幾節的例子是將Cesium默認的相機渲染到紋理&#xff08;RTT&#xff09;或Canvas&#xff0c;這片文章講解如何將自定義的一個camera的畫面渲染到Canvas上&#xff0c;有了前面幾篇的基礎了&#xff0c;也能將自定義的畫面渲染紋理、也可以灰度處理&#xff0c;原理是一樣的…

雙機并聯無功環流抑制虛擬阻抗VSG控制【simulink仿真模型實現】

雙機并聯虛擬同步發電機&#xff08;VSG&#xff09;系統中&#xff0c;因線路阻抗不匹配及參數差異&#xff0c;易引發無功環流。本方案在傳統VSG控制基礎上&#xff0c;引入自適應虛擬阻抗環節。其核心在于&#xff1a;實時檢測兩機間無功環流分量&#xff0c;據此動態調節各…

python測試總結

測試題的基礎知識點總結 1.循環求和 for循環步長&#xff08;range(2,101,2)&#xff09; while循環條件判斷&#xff08;i%20&#xff09; 生成器表達式&#xff08;sum(i for i in range )&#xff09; 所以&#xff1a;sum(range(1,101,2))&#xff08;奇數和&#xff09;和…

識別和分類惡意軟件樣本的工具YARA

YARA 是一個用于識別和分類惡意軟件樣本的工具,廣泛應用于惡意軟件分析、威脅情報、入侵檢測等領域。它通過編寫規則(YARA Rules)來匹配文件中的特定字符串、十六進制模式、正則表達式等特征。 一、YARA 的基本使用方法 1. 安裝 YARA Linux(Ubuntu/Debian) sudo apt-ge…

GaussDB 約束的語法

1 約束的作用約束是作用于數據表中列上的規則&#xff0c;用于限制表中數據的類型。約束的存在保證了數據庫中數據的精確性和可靠性。約束有列級和表級之分&#xff0c;列級約束作用于單一的列&#xff0c;而表級約束作用于整張數據表。下面是 GaussDB SQL 中常用的約束。NOT …

SecurityContextHolder 管理安全上下文的核心組件詳解

SecurityContextHolder 管理安全上下文的核心組件詳解在 Spring Security 中&#xff0c;SecurityContextHolder 是??安全上下文&#xff08;Security Context&#xff09;的核心存儲容器??&#xff0c;其核心作用是??在當前線程中保存當前用戶的認證信息&#xff08;如用…

c++詳解系列(引用指針)

目錄 1.什么是引用 2.引用的定義 3.引用的特性 4.引用的使用 4.1引用傳參 4.2傳引用返回 5.const引用&#xff08;在引用的定義前用const修飾&#xff09; 5.1對于引用 5.2對于指針 6.引用&指針 總結 1.什么是引用 引用就是給變量起別名&#xff0c;一個變量可以…

深度學習loss總結(二)

對于目前深度學習主流任務學習,loss的設置至關重要。下面就不同任務的loss設置進行如下總結: (1)目標檢測 2D/3D目標檢測中的 Loss(損失函數)是訓練模型時優化目標的核心,通常包括位置、類別、尺寸、方向等多個方面。以下是目前 常見的 2D 和 3D 目標檢測 Loss 分類與…

【Linux網絡】netstat 的 -anptu 各個參數各自表示什么意思?

netstat 是一個網絡統計工具&#xff0c;它可以顯示網絡連接、路由表、接口統計、偽裝連接和多播成員資格。在 netstat 命令中&#xff0c;不同的參數可以用來定制輸出的內容。 你提到的 -anptu 參數組合各自的功能如下&#xff1a; -a (all): 顯示所有活動的連接和監聽端口。它…