js代码执行过程、调用栈、执行上下文
参考资料
极客时间课程《浏览器工作原理与实践》 – 李兵
一、js代码执行过程
(一)javascript代码的执行流程
浏览器执行javascript代码的流程如下图所示:
javascript的执行机制是:先编译,再执行。在编译阶段生成了执行上下文、可执行代码。执行上下文由变量环境、词法环境构成,它为可执行代码在执行时提供运行环境。
在编译阶段,javascript引擎将变量的声明部分和函数的声明部分提升到代码开头,并且给被提升的变量设置默认值:undefined,同时这些被提升的变量将被放入到变量环境对象中。
(二)变量提升(Hoisting)
所谓变量提升就是js代码在编译阶段,js引擎将变量的声明部分和函数的声明部分提升到代码开头的一种行为。在js代码执行前会先进行编译,在编译阶段出现了变量提升,关于编译阶段为什么会出现变量提升、变量提升带来的问题、ES6如何修正变量提升,将在 变量提升 中进行深入了解。下面先对变量提升做初步了解:
下面举例分析一下:
- 例1:常见的变量提升
//示例代码
console.log("fun1", fun1);
showName()
console.log("myname",myname);
var myname = "aaa";
function showName () {console.log("执行showName函数");
}
var fun1 = function () {console.log("另一种函数声明方式");
}//变量提升:
function showName () {console.log("执行showName函数")
}
var myname = undefined;
var fun1 = undefined;//可执行代码:
console.log("fun1", fun1);
showName()
console.log("myname",myname);
myname = "aaa";
fun1 = function () {console.log("另一种函数声明方式");
}//执行结果:
fun1 undefined
执行showName函数
myname undefined
该例子中的变量环境对象大致如下表所示,对于函数声明(function声明),函数体一起被提升,并存在变量环境对象中。当函数被调用时,函数体会被编译,并创建函数执行上下文,为函数运行提供环境。当函数执行完毕,函数执行上下文将被销毁。
变量名 | 值 |
---|---|
showName | function () { console.log(“执行showName函数”); } |
myname | undefined |
fun1 | undefined |
- 例2:变量与函数同名
//示例代码showName()
/** 函数表达式等同变量声明处理,函数声明在前,变量声明在后 **/
function showName () {console.log(1);
}
/** 重新赋值 **/
var showName = function () {console.log(2);
}
showName()//变量提升:
/** 函数提升优先级高于变量提升,先被提升 **/
function showName () {console.log(1);
}
var showName = undefined;//可执行代码:
showName()
showName = function () {console.log(2);
}
showName()//执行结果:
1
2
分析:变量提示时,存在变量showName与函数showName同名,此时:函数提升比变量提升的优先级要高,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。简单来说就是:在进行变量提升时,先提升函数声明,再提升变量声明;当有变量名与函数名相同时,函数声明不会被变量声明覆盖,但在运行阶段,可以通过赋值语句,对该变量进行重新赋值。所以上述代码运行结果与下面代码运行结果一致:
//示例代码
showName()
/** 函数表达式等同变量声明处理,变量声明在前,函数声明在后 **/
/** 重新赋值 **/
var showName = function () {console.log(2);
}
function showName () {console.log(1);
}
showName()//变量提升:
/** 函数提升优先级高于变量提升,先被提升 **/
function showName () {console.log(1);
}
var showName = undefined;//可执行代码:
showName()
showName = function () {console.log(2);
}
showName()//执行结果:
1
2
- 例3:函数与函数同名,js编译阶段会选择最后声明的那个
总结:变量提升只提升声明,不提升赋值,针对变量声明与函数声明进行提升。函数声明主要有:函数声明、函数表达式。其中函数声明会将方法体也提升,而函数表达式同变量提升一样,只会提升声明。当变量提升遇到ES6的let/const,会出现暂时性死区,效果就像没有提升。另外要注意在ES6的块级作用域中,var变量能够穿透块提升到全局。
二、调用栈
栈是一种后进先出的数据结构。在执行js代码时,可能会存在多个执行上下文,这些执行上下文可能是编译全局代码创建的全局执行上下文(全局唯一)、编译函数体创建的函数执行上下文、编译eval函数创建的执行上下文。这些执行上下文之间也可能存在相互调用关系,js引擎采用栈来对这些执行上下文进行管理。通常将这种用于管理执行上下文的栈称为调用栈,也叫做执行上下文栈。
通过代码示例分析调用栈如何管理执行上下文:
-
创建全局执行上下文,生成可执行代码,并将上下文压入栈底
a. 更新全局上下文,a = 2;
b. 调用addAll -
开始编译addAll函数体,生成可执行代码,并将上下文入栈
a. 更新函数上下文,d = 10;
b. 调用add -
开始编译add函数体,生成可执行代码,并将上下文入栈
a. 执行函数返回:return b + c ; // 9 -
add函数上下文被弹出栈,更新addAll函数上下文:result = 9;
-
addAll函数继续执行
a. 执行函数返回:return a + result + d; // 21 -
add函数上下文被弹出栈,无需更新全局上下文
-
全局上下文继续执行
-
执行完毕
总结:js引擎通过执行上下文的变量环境来支持变量提升。
三、变量提升
在深入了解变量提升前,需要先了解一下什么是作用域,以及ES6出现的块级作用域。
(一)作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。简单来说,作用域就是变量与函数的可访问范围,它控制着变量和函数的可见性和生命周期,决定了变量在何处能够被访问,在何时会被销毁。在ES6之前,js只有全局作用域和函数作用域,在ES6时为了解决变量提升带来的一系列问题,引入了块级作用域。以下是在各个作用域中定义的变量的访问范围和生命周期:
- 全局作用域:
- 访问范围:代码中的任何地方
- 生命周期:伴随页面的生命周期
- 函数作用域:
- 访问范围:在函数内部
- 生命周期:在函数执行时存在,执行结束后被销毁
- 块级作用域:
- 访问范围:在{}代码块内部
- 生命周期:在执行{}代码块时存在,执行结束后被销毁
(二)变量提升带来的问题
- 变量容易在不被察觉的情况下被覆盖掉
/** 示例代码 **/
var myname = "1";
function showName () {console.log(myname);if (0) {var myname = "2";}console.log(myname);
}
showName()/** 全局执行上下文 **/
//变量提升
function showName () {console.log(myname);if (0) {var myname = "2";}console.log(myname);
}
var myname = undefined;
//可执行代码
myname = "1";
showName()/** 函数执行上下文 **/
//变量提升
var myname = undefined;
//可执行代码
console.log(myname);
if (0) {myname = "2";
}
console.log(myname);/** 输出结果 **/
undefined
undefined
分析:该代码在ES6出现前,调用函数showName时开始编译showName函数体,if条件中声明的myname由于没有块级作用域而被提升至函数顶部,并放入函数执行上下文中的变量环境对象。在执行可执行代码时,js优先从当前的执行上下文中查找变量。由于当前执行上下文中包含了变量myname,值为undefined,所以输出结果均为undefined。该段代码在有块级作用域的其他语言中,输出结果应当均为"1"。
- 本应被销毁的变量没有被销毁
/** 示例代码 **/
function foo () {for (var i = 0; i < 7; i++) {}console.log(i);
}
foo()/** 全局执行上下文 **/
//变量提升
function foo () {for (var i = 0; i < 7; i++) {}console.log(i);
}
//可执行代码
foo()/** 函数执行上下文 **/
//变量提升
var i = undefined
//可执行代码
for (i = 0; i < 7; i++) {}
console.log(i);/** 输出结果 **/
7
分析:在创建函数执行上下文时,变量i被提升,当for循环结束后,变量i并没有被销毁,它存在函数执行上下文的变量环境对象中。这导致输出结果为7。而在有块级作用域的其他语言中,for循环完毕,变量应当被销毁。
(三)ES6如何修正变量提升
在最初设计js语言时,设计者没有打算把这门语言设计得太复杂,只是引入函数作用域和全局作用域,忽略了一些块级作用域。这样如果变量或者函数在if块、while块里面,因为它们没有作用域,所以在编译阶段,可以直接把这些变量或者函数提升到开头,这样大大降低了设计语言的复杂性,但也埋下了混乱的种子。通过上面变量提升带来的问题,可以了解到在需要有块级作用域的时候,js的运行结果与c、java等拥有块级作用域语言的运行结果截然不同。随着JavaScript的流行,这门最初只为了让网页动起来而设计的语言逐渐暴露出更多的问题,最终为了能够解决这些问题,推出了ES6。
ES6在语言层面做了很大的调整,但为了保持向下兼容,就必须在支持旧规则的同时实现新的规则。针对变量提升来说,我们知道在编译阶段会将变量、函数提升并存放于执行上下文的变量环境对象中,在执行阶段通过在调用栈管理的一个个上下文环境的变量环境对象中查找目标变量/函数。可以说变量环境对象+调用栈支持了变量提升。在ES6中,为了保持兼容,选择在词法环境中自行维护一个类似调用栈的小型栈来管理块级作用域。在编译阶段,针对let、const关键字声明的变量,都将被放置到词法环境中,而var声明的变量或函数将被放置到变量环境对象中。
(四)从执行上下文角度分析块级作用域
通过代码示例分析,ES6如何结合let关键字支持块级作用域:
-
调用函数时开始编译函数体,只编译一次
-
进行变量提升,将a、c放入变量环境对象,将处于函数内部且非处于块级作用域内的变量压入词法环境维护的小型栈作为栈底。
-
对于{}块级作用域,对b、d进行提升并初始化为undefined,形成的对象暂不压入栈中
-
foo函数体生成可执行代码如下:
a = 1; b = 2; {b = 3;c = 4;d = 5;console.log(a);console.log(b); } console.log(b); console.log(c); console.log(d);
-
执行可执行代码,更新变量环境对象:a = 1;更新词法环境小型栈栈底:b = 2;
-
执行到{}代码块,将块级作用域编译阶段形成的对象压入栈中,作为栈顶。
-
更新词法环境小型栈栈顶:b = 3;
-
从词法环境小型栈栈顶向下查找变量c,找不到则进入变量环境对象查找c。更新变量环境对象:c = 4;
-
更新词法环境小型栈栈顶:d = 5;
-
先查找词法环境,再查找变量环境对象,查找a,打印值
-
打印b,查找方式同10
-
{}代码块执行完毕,将块级作用域编译阶段形成的对象从栈顶弹出。
-
继续执行函数体,打印b,先进入词法环境查找,再进入对象环境查找。
-
打印c
-
打印d时因查找不到变量而抛出异常
-
最终输出结果:1 3 2 4 抛出异常:Uncaught ReferenceError: d is not defined
总结:ES6通过在词法环境中维护一个类似调用栈的小型栈来支持块级作用域。当函数执行时,会将函数最外层通过let、const声明的变量,并且不处于块级作用域内的变量,进行变量提升,初始化为undefined,然后压入栈底。当运行带{}代码块时,再将该代码块通过let、const声明的变量追加入栈;代码块执行结束时又将它弹出栈顶销毁。当需要查找变量时,先从词法环境开始查找,自栈顶向下查找,若未命中目标,将进入变量环境查找。
(五)暂时性死区
在编译阶段已经有词法环境,变量也已经默认初始化为undefined,但在let声明该变量前使用,将会出现暂时性死区。如下:
function foo () {//debuggerconsole.log("a", a);let a = 1;
}
foo()
通过断点可以观察到在编译阶段,变量a被提升到函数顶并初始化为undefined;但上述代码最终执行结果是抛出异常:Uncaught ReferenceError: Cannot access ‘a’ before initialization,而不是打印出undefined。这是js引擎做的一个控制,通过let声明的变量,即使在编译阶段被提升并且初始化为undefined了,但就是不允许在代码执行到let声明前被访问。
总结:暂时性死区是语法规定,虽然通过let声明的变量在代码执行阶段已经存在词法环境中,但在执行到对应的let声明前访问该变量,js引擎就会抛出一个错误。
相关文章:

js代码执行过程、调用栈、执行上下文
参考资料 极客时间课程《浏览器工作原理与实践》 – 李兵 一、js代码执行过程 (一)javascript代码的执行流程 浏览器执行javascript代码的流程如下图所示: javascript的执行机制是:先编译,再执行。在编译阶段生成了…...

互联网摸鱼日报(2023-05-12)
互联网摸鱼日报(2023-05-12) InfoQ 热门话题 建设和改进持续业务交付能力| BizDevOps 公开课 一部手机就可运行,精通Python等20种语言!谷歌终于能与OpenAI 打擂台了,全新PaLM 2比肩GPT-4 蚂蚁数科开发者…...

【Python从入门到实践3.1】扑克发牌知识点(range函数,def函数,else语句配合使用,random库,列表推导式)
扑克发牌知识点 range函数def函数else语句配合使用:random库列表推导式 本篇博文需要特别感谢"Python从入门到精通"课程中一位同学对扑克发牌程序做出的知识点分析,本博文的内容大多也是从这位同学的分析而来. range函数 Range()函数: *返回一…...

Spring Cloud第二季--Spring Cloud Bus
文章目录 Spring Clud Bus什么是总线基本原理 牛刀小试 Spring Clud Bus 在Spring Cloud学习–配置中心(Config)中实现了集中管理微服务配置、不同环境不同配置、运行期间也可动态调整、配置修改后可以自动更新的需求,但同时也有一个弊端&am…...

Unittest自动化测试之unittestunittest_生成测试报告
unittest_生成测试报告 测试报告为测试结果的统计即展示,是自动化测试不可或缺的一部分,利用unittest 可以生成测试报告 方式一、使用第三方 HTMLTestRunner 执行测试用例集,生成网页版测试报告(推荐) HTMLTestRunn…...

一个查询IP地理信息和CDN提供商的离线终端工具
Nali 功能 支持多种数据库 纯真 IPv4 离线数据库ZX IPv6 离线数据库Geoip2 城市数据库 (可选)IPIP 数据库 (可选)ip2region 数据库 (可选)DB-IP 数据库 (可选)IP2Location DB3 LITE 数据库 (可选) CDN 服务提供商查询支持管道处理支持交互式查询同时支持IPv4和IPv6支持多语言…...

RflySim平台使用篇 | Rflysim3D软件使用系列教程(二)
导读: RflySim3D(支持体验版)和RflySimUE5(支持完整版)为本平台核心三维显示软件, 分别基于UE4 和UE5 引擎开发,具备高逼真虚拟现实显示效果。本视频主要讲解了如何将自定义的三维场景如何加载到RflySim3D…...

2023 年第五届河南省 CCPC 大学生程序设计竞赛
题目地址 题目PDF地址 题解地址 Problem A. 小水獭游河南 ∣ a ∣ ≤ ∣ Σ ∣ 26 ,暴力枚举 a 判断 b 是否为是回文串即可,时间复杂度 O ( ∣ Σ ∣ ∣ s ∣ ) 。 |a| ≤ |Σ| 26,暴力枚举 a 判断 b 是否为是回文串即可,时间…...

nginx liunx最新版本安装flask部署
一、nginx安装 1.进入Nginx官网的资源下载页:http://nginx.org/en/download.html 2.下载nginx-1.22.1.tar.gz, 3解压: tar -zxvf nginx-1.22.1.tar.gz解压完成后会在当前目录下得到一个新的nginx文件夹 4.终端进入nginx文件夹目录&#x…...

热图 -- pheatmap or ggplot2
文章目录 brief数据准备 pheatmap实例最朴素的方式数据缩放取消聚类更改每个小方格的大小聚类以及聚类方式和参数修改热图呈现的颜色修改legend ggplot2实例ggplot2实例变式添加 group bar做成dotplot pheatmap 多图组合问题 brief 这里主要记录了pheatmap 以及 ggplot2实现热…...

EIScopus检索 | 2023年智能交通与未来出行国际会议(CSTFM 2023)
会议简介 Brief Introduction 2023年智能交通与未来出行国际会议(CSTFM 2023) 会议时间:2023年7月28日-30日 召开地点:中国长沙 大会官网: CSTFM 2023-2023 International Conference on Smart Transportation and Future Mobility(CSTFM 202…...

如何系列 如何在Windows和Linux安装Nginx
文章目录 Windows一 下载Nginx二 启动Nginx三 验证 Linux一 安装依赖项二 下载Nginx源码包三 安装四 验证五 常用命令附录 Nginx是一款高性能的开源Web服务器和反向代理服务器,被广泛用于构建现代化的Web应用和提供静态内容。本篇博文将教你如何在Windows和Linux操作…...

“1+X+N”模式助力企业数字化转型
近期,中电金信顺利完成某股份制银行“基于战略解析与业务架构的全行科技规划项目”交付。针对客户的实际业务需求,中电金信采用“1XN”服务模式,服务客户全面的企业架构转型规划。项目组联合行方协同创新,首次将企架建模方法应用于…...

JavaEE(系列3) -- 多线程(线程的中断与线程等待)
新内容开始之前,我们总结一个知识点. Thread类中的start方法和run方法的区别? start(): 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程&#…...

想装一台自己的电脑,可以先了解下这些问题
时间:2023年5月11日19:09:56 ✨✨✨问题清单: ↪️计算机中CPU和内存是什么?分别有什么作用? ↪️为什么计算机中要有内存?CPU访问内存中的数据和访问硬盘中的数据有什么差别? ↪️CPU的基准速度表示什…...

Redis未授权漏洞复现
Redis简介 Redis是C语言开发的一个开源高性能(key-value)键值对类型的内存NoSQL数据库,可以用作数据库、缓存、信息中间件(性能非常优秀,支持持久化到硬盘且高可用)。由于其自身特点,可以广泛应用在数据集群ÿ…...

跳槽,如果没有更好的选择,可以去美团试试···
在美团干了半年,说一下自己的感受,美团是一家福利中等,工资待遇中上,高层管理团队强大,加班强度一般,技术不错,办公环境一般,工作氛围中上,部门差距之间工作体验差距巨大…...

Java10
Java10 (一)、配置文件(二)、多线程2.1 并发和并行2.2 多线程的实现方式2.3 常见成员方法2.3.1 线程的优先级2.3.2 守护线程(备胎线程)2.3.3 礼让线程和插入线程 2.4 线程生命周期2.5 线程安全问题2.6 锁2.…...

IMS call通话类型对比差异
IMS call呼入/呼出流程对比 呼出MO call大致流程 1)UE发送INVITE消息发起IMS call 2)UE接收网络返回的100 Trying 3)UE接收183 Session Progress 4)UE发送PRACK确认收到183 5)UE接收200 OK(PRACK) 6)UE发送UPDATE进行precondition流程 7)UE接收200 OK(UPDATE) 8…...

5.2 中心极限定理
学习目标: 要学习中心极限定理,我会采取以下几个步骤: 学习基本概念:了解什么是随机变量、样本、总体、概率密度函数等基本概念,为学习中心极限定理打下基础;学习正态分布:中心极限定理的核心…...

JVM 内存分哪几个区,如和判断一个对象是否存活
JVM 内存分哪几个区,每个区的作用是什么? java 虚拟机主要分为以下一个区:方法区: 1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型…...

在Spring Boot微服务使用Jedis操作Redis List列表
记录:408 场景:在Spring Boot微服务使用Jedis操作Redis List列表。 版本:JDK 1.8,Spring Boot 2.6.3,redis-6.2.5,jedis-3.7.1。 1.微服务中配置Redis信息 1.1在application.yml中Jedis配置信息 hub:example:redis:jedis:host: 192.168.…...

springboot + vue 部署 阿里云云服务器 ECS
安装所需文件 安装mysql5.7 下载MySQL的yum源配置 wget http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm安装MySQL的yum源 yum -y install mysql57-community-release-el7-11.noarch.rpm使用yum方式安装MySQL5.7(下载需要点时间…...

mysql 日期 计算 时间差 天数差
mysql计算两个日期的时间差 第一种:TIMESTAMPDIFF函数 三个参数。第一个参数是比较的类型:FRAC_SECOND、SECOND、 MINUTE、 HOUR、 DAY、 WEEK、 MONTH、 QUARTER、YEAR几种类型。第二、三参数是时间,后减前: SELECT TIMESTAMPDIFF(DAY,20…...

不用网闸、FTP的话 如何实现内外网数据交换?
网络隔离已然成为很多企业首选的数据保护方式,即使是内部人员之间,也是不能随意的发送敏感文件的。但是,文件的流转交互,又是不可避免的,网络隔离保障了企业网络安全,但在具体实践中仍需解决各隔离网间的数…...

探寻Spring MVC的奥秘:内部组件与工作流程详解
Spring MVC是一个基于MVC架构模式的Web框架,是Spring框架的一个组件。它提供了一套Web应用程序开发的全面解决方案,包括从请求到响应的处理流程、处理请求的控制器、视图解析器、国际化和验证器等。 在这篇文章中,我们将介绍Spring MVC框架的…...

eclipse svn ClassNotFoundException: javassist.ClassPool
eclipse 五月 10, 2023 9:26:49 上午 org.apache.catalina.core.StandardContext filterStart 严重: Exception starting filter struts2 java.lang.reflect.InvocationTargetException - Class: com.opensymphony.xwork2.inject.ContainerImpl M e t h o d I n j e c t o r F…...

广度优先遍历搜索迷宫最短路径
思路分析 由于广度是扩散逐层的方式遍历,相当于是多条路同时跑,最后先到终点就是最短路径了。 广度优先搜索主要使用队列来进行处理 路径用一个单独的vector存储,每一个点的坐标由二维转为一维,如(2, 3)存储在vector中下标为2*…...

分布式计算基础知识
分布式系统的概念 分布式系统是由多个独立计算机组成的系统,这些计算机通过网络进行通信和协作,共同完成一个任务。分布式系统的特点是具有高可用性、可扩展性和容错性。 在分布式系统中,每个计算机节点都可以独立地执行任务,同…...

Mybatis方式完成CRUD操作
Mybatis方式完成CRUD操作 文章目录 Mybatis方式完成CRUD操作1、java以Mybatis方式操作DB1.1、配置数据源-创建 resources/mybatis-config.xml1.2、创建java bean-Monster1.3、配置Mapper接口声明方法1.4、配置xxMapper,完成SQL配置,实现CRUD操作1.5、Test测试 2、需…...