文章目录

  • 一、为什么需要作用域?
  • 二、什么是 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): 显示所有活动的连接和监听端口。它…