【js基础巩固】深入理解作用域与作用域链

news2024/9/20 14:23:57

作用域链

先看一段代码,下面代码输出的结果是什么?

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

当执行到 console.log(myName) 这句代码的时候,其调用栈的状态图如下所示:

JS执行机制 - 作用域链和闭包

此时,全局执行上下文foo函数的执行上下文都包含变量myName,那么 bar函数里面的myName值到底该选择哪个呢?

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

  1. 先查找栈顶的函数上下文中是否存在myName变量,这里没有,所以接着往下查找foo函数上下文中的变量;
  2. foo函数中找到了myName变量,这时就使用foo函数中的myName

如果按照这种方式来查找变量,那么最终执行bar函数打印出来的结果就应该是“极客邦”。但实际情况并非如此,而是打印“极客时间”。要解释这个问题,就涉及到作用域链了。

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

当一段代码使用了一个变量时,JS引擎首先会在“当前的执行上下文”中查找该变量,如果当前的变量环境中仍没有查找到,那么JS引擎会继续在outer所指向的执行上下文中查找。

JS执行机制 - 作用域链和闭包

如前面的代码示例,bar函数和foo函数的outer都是指向全局上下文的。

如果在bar函数或者foo函数中使用了外部变量,那么JS引擎会去全局执行上下文中查找。这个查找的链条就称为作用域链

那么上面的两个函数的 outer 为什么指向全局上下文呢? 这是因为JS在执行过程中,其作用域链是由词法作用域决定的(而非调用栈)。

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

JS执行机制 - 作用域链和闭包

如上图函数的定义位置所示,整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域

块级作用域中的变量查找

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

要想得出其执行结果,就要站在作用域链的角度来分析下其执行过程。当执行到代码块时,如果代码块中有 let 或者 const 声明的变量,那么变量就会存放到该函数的词法环境中。对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的情况如下图所示:

JS执行机制 - 作用域链和闭包

test变量的查找过程如图中标注的1、2、3、4、5序号所示。

首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。 在全局作用域中,是按照词法环境变量环境的顺序查找。

闭包

先看一段代码:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var 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这行代码时,调用栈如下图所示:

JS执行机制 - 作用域链和闭包

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

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

JS执行机制 - 作用域链和闭包

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

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

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

同时有两个函数的作用域链指针(外函数和内函数)指向了同一个变量环境对象,所以无论你删除其中任何一个指针,该变量环境对象都无法被垃圾回收(无论是标记清除还是计数法)清除,所以保存在了内存中。所以就有了所谓的”闭包“

这些闭包是如何使用的呢?当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JS 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量。

JS执行机制 - 作用域链和闭包

setName 的执行上下文中没有 myName 变量,foo 函数的闭包中包含了变量 myName,所以调用 setName 时,会修改 foo 闭包中的 myName 变量的值。同样的流程,当调用 bar.getName 的时候,所访问的变量 myName 也是位于 foo 函数闭包中的。

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

JS执行机制 - 作用域链和闭包

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

闭包是怎么回收的

如果闭包使用不正确,会很容易造成内存泄漏。

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

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

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


延伸
以下函数的输出结果是什么? 会产生闭包吗?

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "极客时间"
    return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()

bar 不是一个函数,因此 bar 当中的 printName 其实是一个全局声明的函数,bar 当中的 myName 只是对象的一个属性,也和 printName 没有联系,如果要产生联系,需要使用 this 关键字,表示这里的 myName 是对象的一个属性,不然的话,printName 会通过词法作用域链去到其声明的环境,函数在被创建的时候它的作用域链就已经确定了,所以不论是直接调用bar对象中的printName方法还是在foo函数中返回的printName方法,他们的作用域链都是 自身->全局作用域,自身没有myName就到全局中找,找到了全局作用域中的myName = ' 极客邦',所以两次打印都是“极客邦”啦~

不会产生闭包。

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

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

相关文章

acwing 291.蒙德里安的梦想

解法: 核心:先放横着的,再放竖着的。 总方案数,等于只放横着的小方块的合法方案数。 如何判断当前方案是否合法?所有剩余位置,能否填充满竖着的小方块。 即按列来看,每一列内部所有连续的空着的…

第六十八回 东平府误陷九纹龙 宋公明义释双枪将-文心大模型ernie-speed免费使用方法

宋江和卢俊义抓阄儿,宋江打东平府,卢俊义打东昌府,谁先打下谁做梁山泊主。宋江带领林冲、花荣、刘唐等二十八人,卢俊义带领吴用、公孙胜、关胜等二十八人。 宋江等人到了东平府外安山镇,郁保四和王定六自告奋勇去下战…

搭建NEMU与QEMU的DiffTest环境(动态库方式)

搭建NEMU与QEMU的DiffTest环境(动态库方式) 1 DiffTest原理简述2 编译NEMU3 编译qemu-dl-difftest3.1 修改NEMU/scripts/isa.mk3.2 修改NEMU/tools/qemu-dl-diff/src/diff-test.c3.3 修改NEMU/scripts/build.mk3.4 让qemu-dl-difftest带调试信息3.5 编译…

昇思12天

FCN图像语义分割 1. 主题和背景 FCN是由UC Berkeley的Jonathan Long等人于2015年提出的,用于实现图像的像素级预测。 2. 语义分割的定义和重要性 语义分割是图像处理和机器视觉中的关键技术,旨在对图像中的每个像素进行分类。它在很多领域有重要应用…

【问题解决】 pyocd 报错 No USB backend found 的解决方法

pyocd 报错 No USB backend found 的解决方法 本文记录了我在Windows 10系统上遇到的pyocd命令执行报错——No USB backend found 的分析过程和解决方法。遇到类似问题的朋友可以直接参考最后的解决方法,向了解问题发送原因的可以查看原因分析部分。 文章目录 pyoc…

ChatGPT-4o大语言模型优化、本地私有化部署、从0-1搭建、智能体构建等高级进阶

目录 第一章 ChatGPT-4o使用进阶 第二章 大语言模型原理详解 第三章 大语言模型优化 第四章 开源大语言模型及本地部署 第五章 从0到1搭建第一个大语言模型 第六章 智能体(Agent)构建 第七章 大语言模型发展趋势 第八章 总结与答疑讨论 更多应用…

端口被占用,使用小黑框查杀

netstat -ano (查看目前所有被占的端口) netstat -ano|findstr " 8080" 查一下目前被占用的端口号 ,目前我要查的端口号是:8080,注意 后面打8080的时候,要有空格,要不然报错 **task…

无人机便携式侦测干扰设备(定全向)技术详解

无人机便携式侦测干扰设备(定全向)是一种专门针对无人机进行侦测和干扰的设备。它具备定向和全向两种工作模式,能够覆盖较宽的频率范围,有效侦测并干扰无人机与遥控器之间的通信信号,从而达到控制或驱离无人机的目的。…

CRT工具

CRT工具 传输位置设置 打开SFTP alt p 命令 ls:远程机器当前目录内容 lls:传输位置文件的目录内容 pwd:远程机器的当前位置 lpwd:传输位置的位置 get 文件:ftp传输文件 get -r 文件夹:ftp传输文件…

AI中药处方模型构建与案例

在中医领域,人工智能(AI)可以生成各种指令来辅助诊断、治疗和研究。 1. 诊断辅助指令: 根据患者的症状和体征,自动分析并生成可能的中医证候诊断建议。利用中医望闻问切四诊信息,智能识别关键症状,提供对应的中医辨证思路。2. 治疗建议指令: 根据辨证结果,自动推荐相应…

2024吉他手的超级助手Guitar Pro8中文版本发布啦!

亲爱的音乐爱好者们,今天我要来和你们分享一款让我彻底沉迷的软件—Guitar Pro。如果你是一名热爱吉他的朋友,那么接下来的内容你可要瞪大眼睛仔细看哦!👀🎶 Guitar Pro免费绿色永久安装包下载:&#xff0…

90元搭建渗透/攻防利器盒子!【硬件篇】

前言 以下内容请自行思考后进行实践。 使用场景 在某些情况下开软件进行IP代理很麻烦,并不能实现真正全局,而且还老容易忘记,那么为了在实景工作中,防止蓝队猴子封IP,此文正现。 正文 先说一下实验效果&#xff1…

鸿蒙开发管理:【@ohos.account.distributedAccount (分布式帐号管理)】

分布式帐号管理 本模块提供管理分布式帐号的一些基础功能,主要包括查询和更新帐号登录状态。 说明: 本模块首批接口从API version 7开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。开发前请熟悉鸿蒙开发指导文档&#xff…

axios取消请求功能CancelToken原理解析

今天在论坛看到一个取消axios请求的功能,具体场景是这样的:前端接口以较短时间间隔请求后端接口刷新状态,接口返回“成功”状态之后还有处于pending状态的请求在排队,如果请求不取消就会导致前端最后获取到的状态出错,…

C语言实现计算器混合运算(含括号,加减乘除)

功能描述: 实现计算器混合运算基本功能,包括:( )、、-、*、/。 功能很简单,但实现起来还是有一定逻辑难度,因为,混合运算需要考虑优先级。 优先级最高的为:() 其次&a…

LabVIEW干涉仪测向系统

开发了一套基于LabVIEW的软件系统,结合硬件设备,构建一个干涉仪测向实验教学平台。该平台应用于信号处理课程,帮助学生将理论知识与实际应用相结合,深化对信号处理核心概念的理解和应用。 项目背景: 当前信号处理教学…

基于Wireshark和TiWsPC(Wireshark Packet Converter)的Zigbee抓包

前言 介绍几种Zigbee抓包方式: 1. Ubiqua 使用教程网上非常多也非常清晰; 但是Ubiqua是收费软件,较贵; 我安装过了,费好多事,没安装成功。 2. Killerbee套件 https://github.com/riverloopsec/killerbe…

TB作品】51单片机 Proteus仿真 51单片机SPI显示OLED字符驱动

// GND 电源地 // VCC 接5V或3.3v电源 // D0 P1^4(SCL) // D1 P1^3(SDA) // RES 接P12 // DC 接P11 // CS 接P10 OLED显示接口与控制实验报告 背景 OLED(有机发光二极管)显示器由于其高对比度、低功耗和…

【论文通读】RuleR: Improving LLM Controllability by Rule-based Data Recycling

RuleR: Improving LLM Controllability by Rule-based Data Recycling 前言AbstractMotivationSolutionMethodExperimentsConclusion 前言 一篇关于提升LLMs输出可控性的短文,对SFT数据以规则的方式进行增强,从而提升SFT数据的质量,进而间接帮…

关于模拟数字模块认知

工业上常见信号分类 PLC控制系统主要是电信号 电信号分为数字信号和模拟信号 PLC系统中有数字量模块DO/DI,模拟量AO/AI。(O为输出,I为输入) 在模块应用中,注意前连接器要和冷压端子相匹配。前连接器可理解为接插件&am…