作用域链精讲

作用域链精讲

  • 1编译阶段
    • 1.1分词
    • 1.2解析(解析为抽象语法树AST)
    • 1.3代码生成
  • 2执行阶段
  • 3查询阶段
  • 4嵌套机制(这个比较重要)----就近原则
  • 5异常
    • 5.1计算机为啥要区分LHS和RHS
    • 5.2RHS查询
    • 5.3LHS查询
  • 6什么是词法作用域
  • 7遮蔽效应
  • 8变量和函数的声明提升(也是预解析)
    • 8.1变量的声明提升
    • 8.2函数的声明提升
  • 9作用域链和执行环境
    • 9.1自由变量
    • 9.2上下文执行(执行环境)
    • 9.3执行环境栈
    • 9.4分析一下整个过程
  • 10实例

JavaScript有一套良好的机制用来存储变量,方便变量的查找,这套规则被称作作用域链机制。作用域的内部原理分为编译执行查询嵌套异常5个部分,下面对这5部分进行详细介绍。

1编译阶段

编译过程有3步:分词、解析和代码生成。下面以var a = 2;为例进行这3个过程的说明。

1.1分词

把字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。词法单元组成词法单元流数组。

{"var""keyword",//关键字"a""indentifier",//标识符"=""assignment",//分配"2""integer",//整数";""eos",//结束语句
}

1.2解析(解析为抽象语法树AST)

把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称作“抽象语法树”(Abstract Syntax Tree, AST)。

{operation: "=",left: {keyword: "var",right: "a"},right: "2"
}

在这里插入图片描述

1.3代码生成

把AST转换成可执行代码(机器指令)的过程称作代码生成。

JS引擎的编译过程其实很复杂,上面的三个是最基本的步骤,任何代码片段在执行前都要先进行编译,这个过程很短,通常是在代码执行前的几微妙完成。

2执行阶段

var a=2;
console.log(a);
console.log(b);

如上,以var a=2;为例,JS引擎会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
如果最终找到了该变量,就会将值2赋值给当前的变量a;否则引擎就会抛出异常;

3查询阶段

主要两种查询:左侧查询LHS,右侧查询RHS
可简单理解为变量出现在赋值操作符左边时进行LHS查询,它试图找到变量在内存中的引用地址。当变量出现在赋值操作符的右侧时进行RHS查询,它试图找到一个的具体的数据值。
如下:

function foo(a){console.log(a)
}
foo(2)

解析一下上面代码的过程:首先第一步进行编译,将所有代码生成计算机可识别的指令(这个不用多说); 然后就开始执行阶段,在执行阶段需要查询(运行foo(2)中需要进行查询)。

  • foo()对foo函数对象进行RHS查询
  • 函数传参a=2对a进行了LHS引用
  • console.log(a)需要对console对象进行RHS查询,并且检查里面有没有log方法
  • 接着需要对a进行RHS查询,把得到的值传给了console.log(a);

ps:区分LHS和RHS

  • console.log(a),这里对a的引用是一个RHS引用,因为这里a并没有赋予任何值,我们只是想查找并取得a的值,然后将它打印出来。
  • a=2;这里对a的引用是一个LHS引用,因为我们并不关心当前的值是什么,只是想要为赋值操作找到目标。

简而言之,在这里将RHS理解为取值,LHS理解为找对象。 在这里,也会发现RHS与getter非常类似,LHS与setter相对应。(表达不严谨,只是为了方便初期记忆。)

为了更便于理解,再举个例子:

function foo(a){  // 2. LHS找a的位置,给a赋值2;var b = a;     // 3. RHS找a的值 4. LHS找b的位置,给b赋a的值2;return a + b;  // 5. RHS找a的值 6. RHS找b的值;
};
var c = foo(2) // 1. RHS找foo的值 7. LHS找c的位置,给c赋值foo(2)的值4

4嵌套机制(这个比较重要)----就近原则

作用域链讨论的查找标识符(变量和函数)的问题.当我们使用一个变量时候,js解释器会优先在当前作用域中寻找变量,如果找到了就直接使用;如果没找到就去上一层作用域中去寻找;如果在上一级找到了则使用,没找到就继续去更上一级寻找,依次类推。直到在全局作用域中都没找到,则会报错。

5异常

5.1计算机为啥要区分LHS和RHS

是因为在变量还没有被声明的情况下(在任何作用域中都找不到该变量时),尝试对变量进行LHS和RHS引用会出现不同的错误。

5.2RHS查询

当RHS查询时,如果查询失败,引擎抛出ReferenceError(引用错误)异常:

function foo(a){a = b;  
}
foo();//ReferenceError: b is not defined

如果RHS找到了该变量,但打算对这个变量的值进行不合理的操作(比如试图给一个非函数类型的值进行函数调用,或者引用null或undifined类型的值中的属性),那么引擎或抛出另一种类型的异常,叫做TypeError

ReferenceError同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

5.3LHS查询

当引擎执行LHS查询时,如果在全局作用域中都没有查找到该变量,那么引擎就会要求全局作用域创建一个新的变量,并让作用域将这个变量返还给自己。前提是程序运行在非“严格模式”下。“严格模式”下,不成功的LHS引用也会抛出ReferenceError异常。

// 示例1
function foo(){a = 1;  
}
foo();
console.log(a);//1// 示例2
function foo(){'use strict';a = 1;  
}
foo();
console.log(a);//ReferenceError: a is not defined

6什么是词法作用域

作用域有两种—动态作用域和词法作用域。js是词法作用域

ps:词法作用域和动态作用域分别是什么

  • 词法作用域:函数定义时,即确定的作用域。js中的作用域链,在函数声明时候,就已经确定了,无论函数在何处调用,其作用域变量的查找都是按照定义的包含关系去查找的。
  • 动态作用域:变量的作用域与函数的调用地点有关,在不同的函数中调用,变量的查找会沿着调用函数向上查找。

如图,在全局作用域(区域1)声明的变量只有foo;
在foo作用域(区域2)内声明的变量有a,b,bar;
在bar作用域(区域3)内声明的变量有c;
在这里插入图片描述
再举个例子:

var a = 2;
function foo() {console.log(a);
}
function bar() {var a = 3;foo();
}
bar();

在词法作用域,a的值会先在foo中查找,没有的话到全局中查找,所以a=2。
如果在动态作用域中,a的值先在foo中查找,没有的话到其调用函数中查找,即bar中查找,a=3。
总结:‌词法作用域是一种在编写代码时确定的静态作用域规则。变量的作用域是在代码编写阶段确定的,而不是在运行时根据程序的流程信息动态确定的。

7遮蔽效应

JavaScript函数的遮蔽效应(或称作变量屏蔽)是指在函数作用域内定义的变量会隐藏(或遮蔽)同名的全局变量。换句话说,当使用函数作用域内的变量时,会优先使用函数作用域内的变量,而不是全局变量。

var x = 10;
function foo() {var x = 20;console.log(x); // 输出20
}
foo();
console.log(x); // 输出10,不是20,要特别注意。因为foo中的x这个变量是在foo作用域内的

8变量和函数的声明提升(也是预解析)

8.1变量的声明提升

提升的是声明而不是赋值!!!使用var声明的变量,会在所有代码执行前被声明(没赋值)。

console.log(b)//会打印undefined,不会报错
var b=2;

8.2函数的声明提升

使用函数声明(function关键字)创建的函数,会在所有代码执行前被创建,由此可在函数声明之前调用函数。(用表达式声明的函数则不行)。

思考:在javascript中当使用var声明变量和function声明函数变量名发生冲突时,那个优先级会更高?
函数声明的优先级高于变量声明,即使变量声明在函数声明之前编写。

9作用域链和执行环境

9.1自由变量

在当前作用域中存在但未在当前作用域中声明的变量。一旦有自由变量,就一定存在作用域链,我们需要根据作用域链的查找机制来查找该自由变量。

9.2上下文执行(执行环境)

每个执行环境都有一个与之关联的变量对象,在环境中定义的函数和变量都保存在这个对象中。如下:

var a=1;
var b=2;
function fn(x){var a=10;function bar(x){var a=100;b=x+a;//这个b就是自由变量return b;}bar(20);bar(200);
}
fn(0)

如上:
f(0)的执行环境是:

x:0
a:undefiend(这里a会变量提升)
bar:function
arguments:[0]//没有执行,所以这时候是0个参数
this:window

bar(20)的执行环境是:

x:20
a:undefiend(这里a也会变量提升)
arguments:[0]//没有执行,所以这时候是0个参数
this:window
没有b,b是自由变量

bar(200)的执行环境是:

x:200
a:undefiend(这里a也会变量提升)
arguments:[0]//没有执行,所以这时候是0个参数
this:window
没有b,b是自由变量

9.3执行环境栈

其实就是压栈和出栈的过程。

var a=1;
var b=2;
function fn(x){var a=10;function bar(x){var a=100;b=x+a;//这个b就是自由变量return b;}bar(20);bar(200);
}
fn(0)

如上代码,我们只分析执行bar(20)那一刻的执行环境栈,如下:
在这里插入图片描述
我们会发现这一时刻,这个执行环境栈中有三个执行环境----全局执行环境,fn(0)执行环境,bar(20)执行环境。其中bar(20)执行环境处于活跃状态。

此时bar(20)执行环境处于活跃状态。fn(0)和全局执行环境已经是非活跃状态(已经执行过了)。所以fn(0)执行环境中a此时为10,全局执行环境中此刻a为1,b为2。

9.4分析一下整个过程

在这里插入图片描述
如上图,分析一下整个过程。
一开始栈中只有全局执行环境:
在这里插入图片描述
执行完第2行代码后,全局执行环境下的a和b已经赋值了:
在这里插入图片描述
执行完第13行代码后,执行环境栈中新增了fn(0)执行环境(此时fn(0)执行环境处于活跃状态):
在这里插入图片描述
执行完第13行代码后,fn(0)执行环境中的a被赋值为10;
在这里插入图片描述
执行完第10行代码后,执行环境栈中新增了bar(20)执行环境(此时bar(20)执行环境处于活跃状态):
在这里插入图片描述
执行完第8行代码后,bar(20)执行环境中的a被赋值为100,全局中的b被赋值为120
在这里插入图片描述
执行完第9行代码后,bar(20)执行环境要出栈(此时fn(0)执行环境处于活跃状态):
在这里插入图片描述
执行完第11行代码后,执行环境栈中新增了bar(200)执行环境(此时bar(200)执行环境处于活跃状态),注意此时全局中的b已经是120了:
在这里插入图片描述
执行完第8行代码后,bar(200)执行环境中的a被赋值为100,全局中的b被赋值为300
在这里插入图片描述
执行完第9行代码后,bar(200)执行环境要出栈(此时fn(0)执行环境处于活跃状态):
在这里插入图片描述
执行完第12行代码后,fn(0)执行环境要出栈(此时全局执行环境处于活跃状态)
在这里插入图片描述
执行完毕后,全局执行环境也会出栈:
在这里插入图片描述

10实例

实例1:

var a = 1;
function fn1() {a = 2;//a是自由变量console.log(a)//2
}
fn1();
console.log(a)//2

实例2:

var b = 1;
function fn2() {var b = 2;console.log(b)//2
}
fn2();
console.log(b)//1,这里要注意与1的区别,此时fn2()执行环境已经出栈了

实例3:

//定义形参,相当于在函数中声明对应的变量
var c=1;
function fn3(c){console.log(c);//undefinedc=2;console.log(c);//2
}
fn3()
console.log(c)//1,这里要注意与1的区别,此时fn2()执行环境已经出栈了

实例4:

var d=1;
function fn4(d){console.log(d);//10d=2;console.log(d);//2
}
fn4(10)
console.log(d)//1

实例5:

var e=1;
function fn5(e){console.log(e);//1e=2;console.log(e);//2
}
fn5(e)
console.log(e)//1

实例6:

console.log(f);//function{alert(2)}
var f=1;
console.log(f);//1
function f(){alert(2)
}
console.log(f)//1
var f=3;
console.log(f);//3
var f=function (){alert(4)
}
console.log(f)//function{alert(4)}
var f;
console.log(f)//function{alert(4)}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/69997.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4.【线性代数】——矩阵的LU分解

四 矩阵的LU分解 1. AB的逆矩阵2. 转置矩阵3. ALU3.1 2x2矩阵3.2 3x3矩阵3.3 nxn的矩阵分解的次数? 1. AB的逆矩阵 { ( A B ) ( B − 1 A − 1 ) I ( B − 1 A − 1 ) ( A B ) I ⇒ ( A B ) − 1 B − 1 A − 1 \begin{cases} (AB)(B^{-1}A^{-1}) I\\ (B^{-1}A^…

Arduino-ESP8266 GPIO(中断或轮询)

检测GPIO高低电平 1. 中断 2. 轮询 gpio.ino // GPIO按键输入 // 监听高电平接线图 // ESP8266 NodeMCU // ┌───────────┐ // │ D1(GPIO5) │──────┤按键一端 // │ │ │ // │ 3V3 │──────┤按键另一端 // └───…

FPGA简介|结构、组成和应用

Field Programmable Gate Arrays(FPGA,现场可编程逻辑门阵列),是在PAL、GAL、CPLD等可编程器件的基础上进一步发展的产物, 是作为专用集成电路(ASIC)领域中的一种半定制电路而出现的&#xff0c…

iOS 获取设备占用内存

获取应用占用内存 获取应用进程占用内存 - (NSUInteger)memoryUsage {task_vm_info_data_t vmInfo;mach_msg_type_number_t count TASK_VM_INFO_COUNT;kern_return_t result task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);if (result …

WPF的MVVMLight框架

在NuGet中引入该库&#xff1a; MVVMLight框架中的命令模式的使用&#xff1a; <StackPanel><TextBox Text"{Binding Name}"/><TextBox Text"{Binding Title}"/><Button Content"点我" Command"{Binding ShowCommand…

如何使用OPENAI的Whisper功能进行音频字母提取功能

首先你可以使用 Python 中的 requests 库来下载该音频文件&#xff0c;然后通过 open() 打开该文件并传递给 OpenAI Whisper API。 完整代码如下&#xff1a; 安装需要的库&#xff1a; pip install openai requests Python 代码&#xff1a; OPENAI_API_KEY "your o…

地平线征程6全球首发上车比亚迪,开启大规模量产交付

2月10日&#xff0c;比亚迪举办智能化战略发布会&#xff0c;对外正式公布全民智驾战略&#xff0c;并发布全新天神之眼高阶智驾系统。未来&#xff0c;比亚迪全系车型将搭载天神之眼高阶智驾系统。 值得注意的是&#xff0c;地平线最新一代车载智能计算方案征程6系列全球首发…

深度学习04 数据增强、调整学习率

目录 数据增强 常用的数据增强方法 调整学习率 学习率 调整学习率 ​调整学习率的方法 有序调整 等间隔调整 多间隔调整 指数衰减 余弦退火 ​自适应调整 自定义调整 数据增强 数据增强是通过对训练数据进行各种变换&#xff08;如旋转、翻转、裁剪等&#xff09;&am…

常用查找算法整理(顺序查找、二分查找、插值查找、斐波那契查找、哈希查找、二叉排序树查找、平衡二叉树查找、红黑树查找、B树和B+树查找、分块查找)

常用的查找算法&#xff1a; 顺序查找&#xff1a;最简单的查找算法&#xff0c;适用于无序或数据量小的情况&#xff0c;逐个元素比较查找目标值。二分查找&#xff1a;要求数据有序&#xff0c;通过不断比较中间元素与目标值&#xff0c;将查找范围缩小一半&#xff0c;效率…

Lineageos 22.1(Android 15) 编译隐藏API的 android.jar

一、前言 有时候会我们开发系统应用需要一些系统的方法或者属性之类的,但是被隐藏导致无法正常显示,因为SDK提供的android.jar被隐藏了,所以只能看到sourcecode,实际上编译是会报错的,比如: 一般这种无法是两种,直接添加一个类,同包名同类名,或者依赖framework.jar,可以骗过…

Game Maker 0.11:《The Sandbox》创作愿景的全新篇章

开放元宇宙已经到来&#xff0c;用户生成内容&#xff08;UGC&#xff09;是其核心。在《The Sandbox》中&#xff0c;我们正在重新定义数字创作&#xff0c;给予新一代创作者工具&#xff0c;打造沉浸式、互动式的游戏和体验&#xff0c;超越传统的短格式内容。在过去的12个月…

(8/100)每日小游戏平台系列

项目地址位于&#xff1a;小游戏导航 新增一个打地鼠游戏&#xff01; 打地鼠&#xff08;Whack-a-Mole&#xff09;是一款经典的休闲游戏&#xff0c;玩家需要点击随机出现的地鼠&#xff0c;以获取分数。游戏时间有限&#xff0c;玩家需要在规定时间内尽可能多地击中地鼠&am…

【动态规划篇】:动态规划中的“双线叙述”--如何用状态转移解决双序列难题

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;动态规划篇–CSDN博客 文章目录 一.双序列类DP解题步骤 二.例题1.最长公共子序列2.不相交的…

观察者模式说明(C语言版本)

观察者模式主要是为了实现一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时&#xff0c;会通知所有观察者对象&#xff0c;使它们能够自动更新自己。下面使用C语言实现了一个具体的应用示例&#xff0c;有需要的可以参考…

yolo11s rknn无法detect的bugfix - step by step

1.缘起 上周四下班时&#xff0c;发现在宿主机环境工作良好的既有的pytorch模型&#xff0c;在通过.pt->.onnx->.rknn的转换后无法正常工作。周五下班时&#xff0c;怀疑疑点在两处&#xff1a; 版本匹配问题通道和参数传递问题。 周六&#xff0c;周日&#xff0c;周…

前端JS接口加密攻防实操

前端JS接口加密攻防实操 背景 在爬虫过程中&#xff0c;对数据接口各类加密的经历总结&#xff0c;无头消耗资源效率不高&#xff0c;采用浏览器兜底解密协程并行 青铜版(混淆对称加密|签名nonce等&#xff09; 解&#xff1a;根据API 调用栈&#xff0c;断点找到request参…

15.3 多线程3

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 15.3.6 线程返回值 如果需要从线程的方法中获得计算的值&#xff0c;可以考虑使用模块级公共变量&#xff0c;在线程对应的方法中最…

同步异步日志系统-日志落地模块的实现

功能&#xff1a;将格式化完成后的日志消息字符串&#xff0c;输出到指定的位置 扩展&#xff1a;支持同时将日志落地到不同的位置 位置分类&#xff1a; 1.标准输出 2.指定文件&#xff08;时候进行日志分析&#xff09; 3.滚动文件&#xff08;文件按照时间/大小进行滚动…

【Kubernetes】k8s 部署指南

1. k8s 入门 1.1 k8s 简介 需要最需要明确的就是&#xff1a;kubernetes&#xff08;简称 k8s &#xff09; 是一个 容器编排平台 &#xff0c;换句话说就是用来管理容器的&#xff0c;相信学过 Docker 的小伙伴对于容器这个概念并不陌生&#xff0c;打个比方&#xff1a;容器…

Spring AI接入DeepSeek:快速打造微应用

随着DeepSeek-R1的官宣开源&#xff0c;DeepSeek迅速成为AI领域的热门话题&#xff0c;吸引了大量开发者和研究者的关注。这一开源举措不仅推动了技术的普及&#xff0c;也促使更多企业和机构加入到开源生态中。例如&#xff0c;国内大厂X度于2月14日宣布将在未来几个月推出文新…