浏览器工作原理与实践--块级作用域:var缺陷以及为什么要引入let和const

在前面《07 | 变量提升:JavaScript代码是按顺序执行的吗?》这篇文章中,我们已经讲解了JavaScript中变量提升的相关内容,正是由于JavaScript存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是JavaScript的一个重要设计缺陷。

虽然ECMAScript6(以下简称ES6)已经通过引入块级作用域并配合let、const关键字,来避开了这种设计缺陷,但是由于JavaScript需要保持向下兼容,所以变量提升在相当长一段时间内还会继续存在。这也加大了你理解概念的难度,因为既要理解新的机制,又要理解变量提升这套机制,关键这两套机制还是同时运行在“一套”系统中的。

但如果抛开JavaScript的底层去理解这些,那么你大概率会很难深入理解其概念。俗话说,“断病要断因,治病要治根”,所以为了便于你更好地理解和学习,今天我们这篇文章会先“探病因”——分析为什么在JavaScript中会存在变量提升,以及变量提升所带来的问题;然后再来“开药方”——介绍如何通过块级作用域并配合let和const关键字来修复这种缺陷。

作用域(scope)

为什么JavaScript中会存在变量提升这个特性,而其他语言似乎都没有这个特性呢?要讲清楚这个问题,我们就得先从作用域讲起。

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

在ES6之前,JavaScript只支持这两种作用域,相较而言,其他语言则都普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

为了更好地理解块级作用域,你可以参考下面的一些示例代码:

//if块
if(1){}//while块
while(1){}//函数块
function foo(){}//for循环块
for(let i = 0; i<100; i++){}//单独一个块
{}

简单来讲,如果一种语言支持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。你可以看下面这段C代码:

char* myname = "极客时间";
void showName() {printf("%s \n",myname);if(0){char* myname = "极客邦";}
}int main(){showName();return 0;
}

上面这段C代码执行后,最终打印出来的是上面全局变量myname的值,之所以这样,是因为C语言是支持块级作用域的,所以if块里面定义的变量是不能被if块外面的语句访问到的。

和Java、C/C++不同,ES6之前是不支持块级作用域的,因为当初设计这门语言的时候,并没有想到JavaScript会火起来,所以只是按照最简单的方式来设计。没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是JavaScript中的变量提升。

变量提升所带来的问题

由于变量提升作用,使用JavaScript来编写和其他语言相同逻辑的代码,都有可能会导致不一样的执行结果。那为什么会出现这种情况呢?主要有以下两种原因。

1. 变量容易在不被察觉的情况下被覆盖掉

比如我们重新使用JavaScript来实现上面那段C代码,实现后的JavaScript代码如下:

var myname = "极客时间"
function showName(){console.log(myname);if(0){var myname = "极客邦"}console.log(myname);
}
showName()

执行上面这段代码,打印出来的是undefined,而并没有像前面C代码那样打印出来“极客时间”的字符串。为什么输出的内容是undefined呢?我们再来分析一下。

首先当刚执行到showName函数调用时,执行上下文和调用栈的状态是怎样的?具体分析过程你可以回顾《08 | 调用栈:为什么JavaScript代码会出现栈溢出?》这篇文章的分析过程,这里我就直接展示出来了,最终的调用栈状态如下图所示:

开始执行showName函数时的调用栈

showName函数的执行上下文创建后,JavaScript引擎便开始执行showName函数内部的代码了。首先执行的是:

console.log(myname);

执行这段代码需要使用变量myname,结合上面的调用栈状态图,你可以看到这里有两个myname变量:一个在全局执行上下文中,其值是“极客时间”;另外一个在showName函数的执行上下文中,其值是undefined。那么到底该使用哪个呢?

相信做过JavaScript开发的同学都能轻松回答出来答案:“当然是先使用函数执行上下文里面的变量啦!”的确是这样,这是因为在函数执行过程中,JavaScript会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量myname,而值是undefined,所以获取到的myname的值就是undefined。

这输出的结果和其他大部分支持块级作用域的语言都不一样,比如上面C语言输出的就是全局变量,所以这会很容易造成误解,特别是在你会一些其他语言的基础之上,再来学习JavaScript,你会觉得这种结果很不自然。

2. 本应销毁的变量没有被销毁

接下来我们再来看下面这段让人误解更大的代码:

function foo(){for (var i = 0; i < 7; i++) {}console.log(i); 
}
foo()

如果你使用C语言或者其他的大部分语言实现类似代码,在for循环结束之后,i就已经被销毁了,但是在JavaScript代码中,i的值并未被销毁,所以最后打印出来的是7。

这同样也是由变量提升而导致的,在创建执行上下文阶段,变量i就已经被提升了,所以当for循环结束之后,变量i并没有被销毁。

这依旧和其他支持块级作用域的语言表现是不一致的,所以必然会给一些人造成误解。

ES6是如何解决变量提升带来的缺陷

上面我们介绍了变量提升而带来的一系列问题,为了解决这些问题,ES6引入了let和const关键字,从而使JavaScript也能像其他语言一样拥有了块级作用域。

关于let和const的用法,你可以参考下面代码:

let x = 5
const y = 6
x = 7
y = 9 //报错,const声明的变量不可以修改

从这段代码你可以看出来,两者之间的区别是,使用let关键字声明的变量是可以被改变的,而使用const声明的变量其值是不可以被改变的。但不管怎样,两者都可以生成块级作用域,为了简单起见,在下面的代码中,我统一使用let关键字来演示。

那么接下来,我们就通过实际的例子来分析下,ES6是如何通过块级作用域来解决上面的问题的。

你可以先参考下面这段存在变量提升的代码:

function varTest() {var x = 1;if (true) {var x = 2;  // 同样的变量!console.log(x);  // 2}console.log(x);  // 2
}

在这段代码中,有两个地方都定义了变量x,第一个地方在函数块的顶部,第二个地方在if块的内部,由于var的作用范围是整个函数,所以在编译阶段,会生成如下的执行上下文:

varTest函数的执行上下文

从执行上下文的变量环境中可以看出,最终只生成了一个变量x,函数体内所有对x的赋值操作都会直接改变变量环境中的x值。

所以上述代码最后通过console.log(x)输出的是2,而对于相同逻辑的代码,其他语言最后一步输出的值应该是1,因为在if块里面的声明不应该影响到块外面的变量。

既然支持块级作用域和不支持块级作用域的代码执行逻辑是不一样的,那么接下来我们就来改造上面的代码,让其支持块级作用域。

这个改造过程其实很简单,只需要把var关键字替换为let关键字,改造后的代码如下:

function letTest() {let x = 1;if (true) {let x = 2;  // 不同的变量console.log(x);  // 2}console.log(x);  // 1
}

执行这段代码,其输出结果就和我们的预期是一致的。这是因为let关键字是支持块级作用域的,所以在编译阶段,JavaScript引擎并不会把if块中通过let声明的变量存放到变量环境中,这也就意味着在if块通过let声明的关键字,并不会提升到全函数可见。所以在if块之内打印出来的值是2,跳出语块之后,打印出来的值就是1了。这种就非常符合我们的编程习惯了:作用域块内声明的变量不影响块外面的变量。

JavaScript是如何支持块级作用域的

现在你知道了ES可以通过使用let或者const关键字来实现块级作用域,不过你是否有过这样的疑问:“在同一段代码中,ES6是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?”

那么接下来,我们就要站在执行上下文的角度来揭开答案。

你已经知道JavaScript引擎是通过变量环境实现函数级作用域的,那么ES6又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?你可以先看下面这段代码:

function foo(){var a = 1let b = 2{let b = 3var c = 4let d = 5console.log(a)console.log(b)}console.log(b) console.log(c)console.log(d)
}   
foo()

当执行上面这段代码的时候,JavaScript引擎会先对其进行编译并创建执行上下文,然后再按照顺序执行代码,关于如何创建执行上下文我们在前面的文章中已经分析过了,但是现在的情况有点不一样,我们引入了let关键字,let关键字会创建块级作用域,那么let关键字是如何影响执行上下文的呢?

接下来我们就来一步步分析上面这段代码的执行流程。

第一步是编译并创建执行上下文,下面是我画出来的执行上下文示意图,你可以参考下:

刚执行时foo函数的执行上下文

通过上图,我们可以得出以下结论:

  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面了。

  • 通过let声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

  • 在函数的作用域块内部,通过let声明的变量并没有被存放到词法环境中。

接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中a的值已经被设置成了1,词法环境中b的值已经被设置成了2,这时候函数的执行上下文就如下图所示:

执行foo函数内部作用域块时的执行上下文

从图中可以看出,当进入函数的作用域块时,作用域块中通过let声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量b,在该作用域块内部也声明了变量b,当执行到作用域内部时,它们都是独立的存在。

其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过let或者const声明的变量。

再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。

这样一个变量查找过程就完成了,你可以参考下图:

变量查找过程

从上图你可以清晰地看出变量查找流程,不过要完整理解查找变量或者查找函数的流程,就涉及到作用域链了,这个我们会在下篇文章中做详细介绍。

当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示:

作用域执行完成示意图

通过上面的分析,想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript引擎也就同时支持了变量提升和块级作用域了。

总结

好了,今天的内容就讲到这里,下面我来简单总结下今天的内容。

由于JavaScript的变量提升存在着变量覆盖、变量污染等设计缺陷,所以ES6引入了块级作用域关键字来解决这些问题。

之后我们还通过对变量环境和词法环境的介绍,分析了JavaScript引擎是如何同时支持变量提升和块级作用域的。

既然聊到了作用域,那最后我们再简单聊下编程语言吧。经常有人争论什么编程语言是世界上最好的语言,但如果站在语言本身来说,我觉得这种争论没有意义,因为语言是工具,而工具是用来创造价值的,至于能否创造价值或创造多大价值不完全由语言本身的特性决定。这么说吧,即便一门设计不那么好的语言,它也可能拥有非常好的生态,比如有完善的框架、非常多的落地应用,又或者能够给开发者带来更多的回报,这些都是评判因素。

如果站在语言层面来谈,每种语言其实都是在相互借鉴对方的优势,协同进化,比如JavaScript引进了块级作用域、迭代器和协程,其底层虚拟机的实现和Java、Python又是非常相似,也就是说如果你理解了JavaScript协程和JavaScript中的虚拟机,其实你也就理解了Java、Python中的协程和虚拟机的实现机制。

所以说,语言本身好坏不重要,重要的是能为开发者创造价值。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

【Pt】马灯贴图绘制过程 03-制作油渍、积尘效果

目录 效果 一、制作油渍效果 1.1 基本油渍 1.2 流淌的油渍痕迹 二、制作浮尘效果 三、制作积尘效果 效果 一、制作油渍效果 1.1 基本油渍 将上篇制作的“锈迹_深色”和“锈迹_浅色”两个文件夹再次合并为一个文件夹 这里就命名为“锈迹” 添加一个填充图层 设置Base …

Redis中的客户端(三)

客户端 身份验证 客户端状态的authenticated属性用于记录客户端是否通过了身份验证: typedef struct redisClient {// ...int authenticated;// ... } redisClient;如果authnticated的值为0&#xff0c;那么表示客户端未通过身份验证&#xff1b;如果authenticated的值为1&a…

快速上手Pytrch爬虫之爬取某应图片壁纸

一、前置知识 1 爬虫简介 网络爬虫&#xff08;又被称作网络蜘蛛、网络机器人&#xff0c;在某些社区中也经常被称为网页追逐者)可以按照指定的规则&#xff08;网络爬虫的算法&#xff09;自动浏览或抓取网络中的信息。 1.1 Web网页存在方式 表层网页指的是不需要提交表单&…

【Java面试题】Redis上篇(基础、持久化、底层数据结构)

文章目录 基础1.什么是Redis?2.Redis可以用来干什么&#xff1f;3.Redis的五种基本数据结构&#xff1f;4.Redis为什么这么快&#xff1f;5.什么是I/O多路复用&#xff1f;6.Redis6.0为什么使用了多线程&#xff1f; 持久化7.Redis的持久化方式&#xff1f;区别&#xff1f;8.…

基于.NET Core开发的轻量级分布式配置中心

前言 今天给大家推荐一个基于.NET Core开发的轻量级分布式配置中心&#xff1a;AgileConfig。 AgileConfig官方介绍 AgileConfig秉承轻量化的特点&#xff0c;部署简单、配置简单、使用简单、学习简单&#xff0c;它只提取了必要的一些功能&#xff0c;并没有像Apollo那样复…

代码随想录算法训练营第36天|738.单调递增的数字|968.监控二叉树|总结

代码随想录算法训练营第36天|738.单调递增的数字|968.监控二叉树|总结 738.单调递增的数字 https://programmercarl.com/0738.%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%95%B0%E5%AD%97.html class Solution { public:int monotoneIncreasingDigits(int n) {string s…

python(一)网络爬取

在爬取网页信息时&#xff0c;需要注意网页爬虫规范文件robots.txt eg:csdn的爬虫规范文件 csdn.net/robots.txt User-agent: 下面的Disallow规则适用于所有爬虫&#xff08;即所有用户代理&#xff09;。星号*是一个通配符&#xff0c;表示“所有”。 Disallow&…

c++中public和private继承怎么影响了变量的使用,今天一篇文章给你讲清楚

文章目录 准则一&#xff1a;继承关系不会改变子类访问基类的变量权限举个例子 准则二&#xff1a;继承关系只会改变基类中的变量继承到子类中后&#xff0c;权限的改变举个例子 准则三&#xff1a;基类中的protected变量在外部是不可访问的&#xff0c;类似private。但可以在继…

【WebJs 爬虫】逆向进阶技术必知必会

前言 在数字化时代&#xff0c;网络爬虫已成为一种强大的数据获取工具&#xff0c;广泛应用于市场分析、竞争对手研究、舆情监测等众多领域。爬虫技术能够帮助我们快速、准确地获取网络上的海量信息&#xff0c;为决策提供有力支持。然而&#xff0c;随着网络环境的日益复杂和…

【热门话题】Yarn:新一代JavaScript包管理器的安装与使用

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 Yarn&#xff1a;新一代JavaScript包管理器的安装与使用引言一、Yarn的安装1. 系…

sonar+gitlab提交阻断 增量扫描

通过本文&#xff0c;您将可以学习到 sonarqube、git\gitlab、shell、sonar-scanner、sonarlint 一、前言 sonarqube 是一款开源的静态代码扫描工具。 实际生产应用中&#xff0c;sonarqube 如何落地&#xff0c;需要考虑以下四个维度&#xff1a; 1、规则的来源 现在规则的…

【学习笔记】java项目—苍穹外卖day01

文章目录 苍穹外卖-day01课程内容1. 软件开发整体介绍1.1 软件开发流程1.2 角色分工1.3 软件环境 2. 苍穹外卖项目介绍2.1 项目介绍2.2 产品原型2.3 技术选型 3. 开发环境搭建3.1 前端环境搭建3.2 后端环境搭建3.2.1 熟悉项目结构3.2.2 Git版本控制3.2.3 数据库环境搭建3.2.4 前…

git配置SSH 密钥

git配置SSH 密钥 1.window配置ssh1.安装ssh2.安装 Git&#xff08;安装教程参见安装Git&#xff09;并保证版本大于 1.9![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e59f4e16b83c45649f1d9d7bd6bf92c0.png)3.SSH 尽量保持最新&#xff0c;6.5之前的版本由于使用…

SpringBoot国际化配置流程(超详细)

前言 最新第一次在做SpringBoot的国际化&#xff0c;网上搜了很多相关的资料&#xff0c;都是一些简单的使用例子&#xff0c;达不到在实际项目中使用的要求&#xff0c;因此本次将结合查询的资料以及自己的实践整理出SpringBoot配置国际化的流程。 SpringBoot国际化 "i…

猫,路由器,WIFI

家庭网络常识 1&#xff1a;猫、路由器、wifi_哔哩哔哩_bilibili 入户光纤插到猫上面&#xff0c;网线连接猫和路由器&#xff0c;网线连接路由器和电脑。路由器可以发射WIFI。 手机通过WIFI连接到路由器。 左边是猫&#xff0c;右边是光猫。 &#xff08;modem&#xff09; …

JAVA面试八股文之集合

JAVA集合相关 集合&#xff1f;说一说Java提供的常见集合&#xff1f;hashmap的key可以为null嘛&#xff1f;hashMap线程是否安全, 如果不安全, 如何解决&#xff1f;HashSet和TreeSet&#xff1f;ArrayList底层是如何实现的&#xff1f;ArrayList listnew ArrayList(10)中的li…

《QT实用小工具·一》电池电量组件

1、概述 项目源码放在文章末尾 本项目实现了一个电池电量控件&#xff0c;包含如下功能&#xff1a; 可设置电池电量&#xff0c;动态切换电池电量变化。可设置电池电量警戒值。可设置电池电量正常颜色和报警颜色。可设置边框渐变颜色。可设置电量变化时每次移动的步长。可设置…

淘宝订单中的涉及红包检测、优惠券检测方案|工具|API

首先&#xff0c;检测订单红包的核心价值是什么&#xff1f; “红包的本质就是薅平台羊毛&#xff1a;不用怀疑&#xff0c;平台对于这种损害平台利益的行为肯定是最高等级的稽查”。那么&#xff0c;在日常运营中&#xff0c;需要尽可能过滤这类订单。 其次&#xff0c;如何使…

堆的应用(堆排序,TOP-K问题)详细讲解

所有人都关心我飞的高不高&#xff0c;只有我妈关心我翅膀硬不硬 一、堆的应用 1. 堆排序 1.1 建堆 1.2 利用堆删除思想来进行排序 2.TOP-K问题 二、完结撒❀ –❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀-正文开始-❀–❀–❀–❀–❀–❀–❀–❀–…

斜率优化dp 笔记

任务安排1 有 N 个任务排成一个序列在一台机器上等待执行&#xff0c;它们的顺序不得改变。 机器会把这 N 个任务分成若干批&#xff0c;每一批包含连续的若干个任务。 从时刻 00 开始&#xff0c;任务被分批加工&#xff0c;执行第 i 个任务所需的时间是 Ti。 另外&#x…