浏览器工作原理与实践--作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的

在上一篇文章中我们讲到了什么是作用域,以及ES6是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念。

理解作用域链是理解闭包的基础,而闭包在JavaScript中几乎无处不在,同时作用域和作用域链还是所有编程语言的基础。所以,如果你想学透一门语言,作用域和作用域链一定是绕不开的。

那今天我们就来聊聊什么是作用域链,并通过作用域链再来讲讲什么是闭包。

首先我们来看下面这段代码:

function bar() {console.log(myName)
}
function foo() {var myName = "极客邦"bar()
}
var myName = "极客时间"
foo()

你觉得这段代码中的bar函数和foo函数打印出来的内容是什么?这就要分析下这两段代码的执行流程。

通过前面几篇文章的学习,想必你已经知道了如何通过执行上下文来分析代码的执行流程了。那么当这段代码执行到bar函数内部时,其调用栈的状态图如下所示:

执行bar函数时的调用栈

从图中可以看出,全局执行上下文和foo函数的执行上下文中都包含变量myName,那bar函数里面myName的值到底该选择哪个呢?

也许你的第一反应是按照调用栈的顺序来查找变量,查找方式如下:

  1. 先查找栈顶是否存在myName变量,但是这里没有,所以接着往下查找foo函数中的变量。

  2. 在foo函数中查找到了myName变量,这时候就使用foo函数中的myName。

如果按照这种方式来查找变量,那么最终执行bar函数打印出来的结果就应该是“极客邦”。但实际情况并非如此,如果你试着执行上述代码,你会发现打印出来的结果是“极客时间”。为什么会是这种情况呢?要解释清楚这个问题,那么你就需要先搞清楚作用域链了。

作用域链

关于作用域链,很多人会感觉费解,但如果你理解了调用栈、执行上下文、词法环境、变量环境等概念,那么你理解起来作用域链也会很容易。所以很是建议你结合前几篇文章将上面那几个概念学习透彻。

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。

当一段代码使用了一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找该变量, 比如上面那段代码在查找myName变量时,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找。为了直观理解,你可以看下面这张图:

带有外部引用的调用栈示意图

从图中可以看出,bar函数和foo函数的outer都是指向全局上下文的,这也就意味着如果在bar函数或者foo函数中使用了外部变量,那么JavaScript引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。

现在你知道变量是通过作用域链来查找的了,不过还有一个疑问没有解开,foo函数调用的bar函数,那为什么bar函数的外部引用是全局执行上下文,而不是foo函数的执行上下文?

要回答这个问题,你还需要知道什么是词法作用域。这是因为在JavaScript执行过程中,其作用域链是由词法作用域决定的。

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

这么讲可能不太好理解,你可以看下面这张图:

词法作用域

从图中可以看出,词法作用域就是根据代码的位置来决定的,其中main函数包含了bar函数,bar函数中包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域—>bar函数作用域—>main函数作用域—>全局作用域。

了解了词法作用域以及JavaScript中的作用域链,我们再回过头来看看上面的那个问题:在开头那段代码中,foo函数调用了bar函数,那为什么bar函数的外部引用是全局执行上下文,而不是foo函数的执行上下文?

这是因为根据词法作用域,foo和bar的上级作用域都是全局作用域,所以如果foo或者bar函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

块级作用域中的变量查找

前面我们通过全局作用域和函数级作用域来分析了作用域链,那接下来我们再来看看块级作用域中变量是如何查找的?在编写代码的时候,如果你使用了一个在当前作用域中不存在的变量,这时JavaScript引擎就需要按照作用域链在其他作用域中查找该变量,如果你不了解该过程,那就会有很大概率写出不稳定的代码。

我们还是先看下面这段代码:

function bar() {var myName = "极客世界"let test1 = 100if (1) {let myName = "Chrome浏览器"console.log(test)}
}
function foo() {var myName = "极客邦"let test = 2{let test = 3bar()}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()

你可以自己先分析下这段代码的执行流程,看看能否分析出来执行结果。

要想得出其执行结果,那接下来我们就得站在作用域链和词法环境的角度来分析下其执行过程。

在上篇文章中我们已经介绍过了,ES6是支持块级作用域的,当执行到代码块时,如果代码块中有let或者const声明的变量,那么变量就会存放到该函数的词法环境中。对于上面这段代码,当执行到bar函数内部的if语句块时,其调用栈的情况如下图所示:

块级作用域中是如何查找变量的

现在是执行到bar函数的if语块之内,需要打印出来变量test,那么就需要查找到test变量的值,其查找过程我已经在上图中使用序号1、2、3、4、5标记出来了。

下面我就来解释下这个过程。首先是在bar函数的执行上下文中查找,但因为bar函数的执行上下文中没有定义test变量,所以根据词法作用域的规则,下一步就在bar函数的外部作用域中查找,也就是全局作用域。

至于单个执行上下文中如何查找变量,我在上一篇文章中已经做了介绍,这里就不重复了。

闭包

了解了作用域链,接着我们就可以来聊聊闭包了。关于闭包,理解起来可能会是一道坎,特别是在你不太熟悉JavaScript这门语言的时候,接触闭包很可能会让你产生一些挫败感,因为你很难通过理解背后的原理来彻底理解闭包,从而导致学习过程中似乎总是似懂非懂。最要命的是,JavaScript代码中还总是充斥着大量的闭包代码。

但理解了变量环境、词法环境和作用域链等概念,那接下来你再理解什么是JavaScript中的闭包就容易多了。这里你可以结合下面这段代码来理解什么是闭包:

function foo() {var myName = "极客时间"let test1 = 1const test2 = 2var innerBar = {getName:function(){console.log(test1)return myName},setName:function(newName){myName = newName}}return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

首先我们看看当执行到foo函数内部的return innerBar这行代码时调用栈的情况,你可以参考下图:

执行到return bar时候的调用栈

从上面的代码可以看出,innerBar是一个对象,包含了getName和setName的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在foo函数内部定义的,并且这两个方法内部都使用了myName和test1两个变量。

根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。所以当foo函数执行完成之后,其整个调用栈的状态如下图所示:

闭包的产生过程

从上图可以看出,foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和getName方法中使用了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中。这像极了setName和getName方法背的一个专属背包,无论在哪里调用了setName和getName方法,它们都会背着这个foo函数的专属背包。

之所以是专属背包,是因为除了setName和getName函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为foo函数的闭包。

好了,现在我们终于可以给闭包一个正式的定义了。在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是foo,那么这些变量的集合就称为foo函数的闭包。

那这些闭包是如何使用的呢?当执行到bar.setName方法中的myName = "极客邦"这句代码时,JavaScript引擎会沿着“当前执行上下文–>foo函数闭包–>全局执行上下文”的顺序来查找myName变量,你可以参考下面的调用栈状态图:

执行bar时调用栈状态

从图中可以看出,setName的执行上下文中没有myName变量,foo函数的闭包中包含了变量myName,所以调用setName时,会修改foo闭包中的myName变量的值。

同样的流程,当调用bar.getName的时候,所访问的变量myName也是位于foo函数闭包中的。

你也可以通过“开发者工具”来看看闭包的情况,打开Chrome的“开发者工具”,在bar函数任意地方打上断点,然后刷新页面,可以看到如下内容:

开发者工具中的闭包展示

从图中可以看出来,当调用bar.getName的时候,右边Scope项就体现出了作用域链的情况:Local就是当前的getName函数的作用域,Closure(foo)是指foo函数的闭包,最下面的Global就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。

所以说,你以后也可以通过Scope来查看实际代码作用域链的情况,这样调试代码也会比较方便。

闭包是怎么回收的

理解什么是闭包之后,接下来我们再来简单聊聊闭包是什么时候销毁的。因为如果闭包使用不正确,会很容易造成内存泄漏的,关注闭包是如何回收的能让你正确地使用闭包。

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

关于闭包回收的问题本文只是做了个简单的介绍,其实闭包是如何回收的还牵涉到了JavaScript的垃圾回收机制,而关于垃圾回收,后续章节我会再为你做详细介绍的。

总结

好了,今天的内容就讲到这里,下面我们来回顾下今天的内容:

  • 首先,介绍了什么是作用域链,我们把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。

  • 其次,介绍了在块级作用域中是如何通过作用域链来查找变量的。

  • 最后,又基于作用域链和词法环境介绍了到底什么是闭包。

通过展开词法作用域,我们介绍了JavaScript中的作用域链和闭包;通过词法作用域,我们分析了在JavaScript的执行过程中,作用域链是已经注定好了,比如即使在foo函数中调用了bar函数,你也无法在bar函数中直接使用foo函数中的变量信息。

因此理解词法作用域对于你理解JavaScript语言本身有着非常大帮助,比如有助于你理解下一篇文章中要介绍的this。另外,理解词法作用域对于你理解其他语言也有很大的帮助,因为它们的逻辑都是一样的。

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

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

相关文章

Verilog语法之assign语句学习

assign语法主要是对组合逻辑的变量进行赋值的,就是把一个变量赋值给另一个变量,被复制的变量必须是wire类型的参数。 从仿真结果可以看出,data_in变量的值赋值给了data_out,assign语法就是赋值没有任何延迟,data_in是什么值&#…

Java数据结构与集合原码

数据结构与集合原码 文章目录 数据结构与集合原码1. 数据结构基本概念1.1 概念1.2 数据结构的研究对象 2. 常见存储结构2.1 数组2.2 链表2.2.1 单向链表2.2.2 双向链表 2.3 二叉树2.4 栈(stack)2.5 队列 3. 二叉树3.1 二叉树的遍历3.2 经典二叉树 4. List实现类源码分析4.1 Arr…

redis和redisson实现分布式锁

redis和redisson实现分布式锁 基于setnx命令的分布式锁基于set命令的分布式锁redission看门狗分布式锁 基于setnx命令的分布式锁 1. 加锁 使用 Redis 实现分布式锁,最直接的想法是利用 setnx 和 expire 命令实现加锁。 在 Redis 中,setnx 是「set if …

1.排列数组奇数在前偶数在后

文章目录 大家好&#xff0c;我是晓星航。今天为大家带来的是 排列数组奇数在前偶数在后 相关的讲解&#xff01;&#x1f600; public static void swap(int[] array) {int left 0;int right array.length - 1;while (left < right) {while (left < right &&…

IDEA2023版本整合SpringBoot热部署

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 开发环境篇 ✨特色专栏&#xff1a; M…

手撕算法-最小覆盖子串

描述 分析 滑动窗口。 参考力扣官方的题解思路 本问题要求我们返回字符串 s 中包含字符串 t 的全部字符的最小窗口。我们称包含 t 的全部字母的窗口为「可行」窗口。 我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针&#xff0c;一个用于「延伸…

javascript基础练习题之渔夫捕鱼

一、题目要求&#xff1a;根据用户输入的年、月、日判断是打鱼还是晒网。代码中使用了isLeapYear函数来判断输入的年份是否为闰年&#xff0c;getDays函数来计算输入日期是一年中的第几天&#xff0c;然后根据计算结果来确定是打鱼还是晒网。最后代码通过弹窗提示用户是打鱼还是…

吴渔夫:AI技术引领游戏产业革命,小团队有大作为

AI技术的突飞猛进&#xff0c;游戏产业正在经历一场前所未有的变革。中国网游先锋&#xff0c;火石控股创始人吴渔夫&#xff0c;近日在接受第一财经日报的采访&#xff0c;对AI在游戏制作中的应用和未来趋势有着深刻的见解。 吴渔夫指出&#xff0c;AI技术的引入极大地降低了游…

游戏推广的新篇章:Xinstall助力实现全渠道效果统计与提升

随着游戏市场的日益繁荣&#xff0c;游戏推广已成为各大游戏公司争夺市场份额的关键环节。然而&#xff0c;面对众多推广渠道和复杂的用户行为&#xff0c;如何精准地评估推广效果、优化投放策略&#xff0c;成为了游戏推广人员亟待解决的问题。此时&#xff0c;Xinstall作为一…

绿岛机械加入2024第13届生物发酵展

参展企业介绍 南京绿岛机械设备有限公司是一家专注于碟式分离机领域的生产服务型企业。公司以多年从事离心分离设备的设计和制造经验为基础&#xff0c;通过产品改良和技术革新&#xff0c;从根本上解决了传统碟式分离设备的固有技术缺陷&#xff0c;增强了控制系统的安全性和…

2024第六届环境科学与可再生能源国际会议能源 (ESRE 2024) 即将召开!

2024第六届环境科学与可再生能源国际会议 能源 &#xff08;ESRE 2024&#xff09; 即将举行 2024 年 6 月 28 日至 30 日在德国法兰克福举行。ESRE 2024 年 旨在为研究人员、从业人员和专业人士提供一个论坛 从工业界、学术界和政府到研究和 发展&#xff0c;环境科学领域的专…

【C++初阶】之类和对象(中)

【C初阶】之类和对象&#xff08;中&#xff09; ✍ 类的六个默认成员函数✍ 构造函数&#x1f3c4; 为什么需要构造函数&#x1f3c4; 默认构造函数&#x1f3c4; 为什么编译器能自动调用默认构造函数&#x1f3c4; 自己写的构造函数&#x1f3c4; 构造函数的特性 ✍ 拷贝构造…

在Windows系统上安装多个 Nodejs

前言 在Windows系统安装Nodejs 在Windows系统上安装多个 Nodejs v14.16.1安装位置 D:\sde\nodejs\node-v14.16.1-win-x64 v16.20.2安装位置 D:\sde\nodejs\node-v16.20.2-win-x64 v18.20.0安装位置 D:\sde\nodejs\node-v18.20.0-win-x64 v20.12.0安装位置 D:\sde\nod…

Java毕业设计-基于springboot开发的游戏分享网站平台-毕业论文+答辩PPT(附源代码+演示视频)

文章目录 前言一、毕设成果演示&#xff08;源代码在文末&#xff09;二、毕设摘要展示1、开发说明2、需求分析3、系统功能结构 三、系统实现展示1、系统功能模块2、后台登录2.1管理员功能模块2.2用户功能模块 四、毕设内容和源代码获取总结 Java毕业设计-基于springboot开发的…

ChatGLM2本地部署方法

chatglm2部署在本地时&#xff0c;需要从huggingface上下载模型的权重文件&#xff08;需要科学上网&#xff09;。下载后权重文件会自动保存在本地用户的文件夹上。但这样不利于分享&#xff0c;下面介绍如何将chatglm2模型打包部署。 一、克隆chatglm2部署 这个项目是chatgl…

“李子园”上榜中国民营企业社会责任优秀案例

日前&#xff0c;由浙江省工商联、浙江工商大学主办&#xff0c;杭州市工商联协办的2024浙江民营企业社会责任暨浙商ESG研讨会在杭州召开&#xff0c;探索民营企业履行社会责任的方法路径和趋势。会上公布了2023年中国民营企业社会责任优秀案例&#xff08;浙江入选企业&#x…

【小尘送书-第十五期】Excel函数与公式应用大全for Excel 365 Excel

大家好&#xff0c;我是小尘&#xff0c;欢迎你的关注&#xff01;大家可以一起交流学习&#xff01;欢迎大家在CSDN后台私信我&#xff01;一起讨论学习&#xff0c;讨论如何找到满意的工作&#xff01; &#x1f468;‍&#x1f4bb;博主主页&#xff1a;小尘要自信 &#x1…

【Linux】对进程地址空间的理解

一、关于进程地址空间的简单理解 进程地址空间其实是分了很多个区域的&#xff0c;区域划分的本质就是区域内的各个地址都是可以使用的。如同下面这个图所示&#xff1a; 无论是环境变量的地址还是环境变量表的地址&#xff0c;所存放的地址都在栈的上部。这里的已初始化数据和…

浅谈性能测试

本文主要针对WEB系统的性能测试。不涉及具体的执行操作&#xff0c;只是本人对性能测试的一点理解和认识。 性能测试的目的&#xff0c;简单说其实就是为了获取待测系统的响应时间、吞吐量、稳定性、容量等信息。而发现一些具体的性能相关的缺陷&#xff08;如内存溢出、并发处…

centos7系统下nginx1.24.0升级

如果没有这些目录&#xff0c;请先创建: mkdir /data/software mkdir /data/program提前下载所需的软件&#xff1a; cd /data/software wget https://ftp.pcre.org/pub/pcre/pcre-8.42.tar.gz wget https://nginx.org/download/nginx-1.24.0.tar.gz安装nginx cd /data/soft…