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

作用域链

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

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 = ' 极客邦',所以两次打印都是“极客邦”啦~

不会产生闭包。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

25_嵌入式系统总线接口

目录 串行接口基本原理 串行通信 串行数据传送模式 串行通信方式 RS-232串行接口 RS-422串行接口 RS-485串行接口 RS串行总线总结 RapidIO高速串行总线 ARINC429总线 并行接口基本原理 并行通信 IEEE488总线 SCSI总线 MXI总线 PCI接口基本原理 PCI总线原理 PC…

Qt | QPen 类(画笔)

01、画笔基础 1、需要使用到的 QPainter 类中的函数原型如下: void setPen(const QPen &pen); //设置画笔,void setPen(const QColor &color); //设置画笔,该笔样式为 Qt::SolidLine、宽度为 1,颜色由 color 指定void setPen(Qt::PenStyle style); //设置画笔,该…

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

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

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

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

Java 应用启动时出现编译错误进程会退出吗?

背景 开发的尽头是啥呢?超超级熟练工! 总结最近遇到的一些简单问题: Java 应用的某个线程,如果运行时依赖的 jar 不满足,线程是否会退出?进程是否会退出?Netty 实现 TCP 功能时,换…

STL复习-序列式容器和容器适配器部分

STL复习 1. 常见的容器 如何介绍这些容器,分别从常见接口,迭代器类型,底层实现 序列式容器 string string严格来说不属于stl,它是属于C标准库 **底层实现:**string本质是char类型的顺序表,因为不同编译…

CC2530寄存器编程学习笔记_点灯

下面是我的CC2530的学习笔记之点灯部分。 第一步:分析原理图 找到需要对应操作的硬件 图 1 通过这个图1我们可以找到LED1和LED2连接的引脚,分别是P1_0和P1_1。 第二步 分析原理图 图 2 通过图2 确认P1_0和P1_1引脚连接到LED,并且这些引…

项目/代码规范与Apifox介绍使用

目录 目录 一、项目规范: (一)项目结构: (二)传送的数据对象体 二、代码规范: (一)数据库命名规范: (二)注释规范: …

【0基础学爬虫】爬虫框架之 feapder 的使用

前言 大数据时代,各行各业对数据采集的需求日益增多,网络爬虫的运用也更为广泛,越来越多的人开始学习网络爬虫这项技术,K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章,为实现从易到难全方位覆盖,特设【0…

mac|Mysql WorkBench 或终端 导入 .sql文件

选择Open SQL Script导入文件 在第一行加入use 你的schema名字,相当于选择了这个schema 点击运行即可将sql文件导入database 看到下面成功了即可 这时候可以看看左侧的目标database中有没有成功导入table,如果没有看到的话,可以点一下右上角的…

Bert入门-使用BERT(transformers库)对推特灾难文本二分类

Kaggle入门竞赛-对推特灾难文本二分类 这个是二月份学习的,最近整理资料所以上传到博客备份一下 数据在这里:https://www.kaggle.com/competitions/nlp-getting-started/data github(jupyter notebook):https://gith…

【JavaEE】多线程进阶

🤡🤡🤡个人主页🤡🤡🤡 🤡🤡🤡JavaEE专栏🤡🤡🤡 文章目录 1.锁策略1.1悲观锁和乐观锁1.2重量级锁和轻量级锁1.3自旋锁和挂起等待锁1.4可…

AI大模型的智能心脏:向量数据库的崛起

在人工智能的飞速发展中,一个关键技术正悄然成为AI大模型的智能心脏——向量数据库。它不仅是数据存储和管理的革命性工具,更是AI技术突破的核心。随着AI大模型在各个领域的广泛应用,向量数据库的重要性日益凸显。 01 技术突破:向量数据库的内在力量 向量数据库以其快速检索…

TypeError: Cannot read properties of null (reading ‘nextSibling‘)

做项目用的Vue3Vite, 在画静态页面时,点击菜单跳转之后总是出现如下报错,百思不得其解。看了网上很多回答,也没有解决问题,然后试了很多方法,最后竟然发现是template里边没有结构的原因。。。 原来我的index.vue是这样…

自注意力 公式解释

公式 (\mathbf{y}_i f(\mathbf{x}_i, (\mathbf{x}_1, \mathbf{x}_1), \ldots, (\mathbf{x}_n, \mathbf{x}_n)) \in \mathbb{R}^d) 描述了自注意力机制中单个词元的输出表示如何生成。我们来逐步解释这个公式: 输入序列 (\mathbf{x}_1, \mathbf{x}_2, \ldots, \math…

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

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

《昇思 25 天学习打卡营第 11 天 | ResNet50 图像分类 》

《昇思 25 天学习打卡营第 11 天 | ResNet50 图像分类 》 活动地址:https://xihe.mindspore.cn/events/mindspore-training-camp 签名:Sam9029 计算机视觉-图像分类,很感兴趣 且今日精神颇佳,一个字,学啊 上一节&…

【数据结构】经典链表题目详解集合(反转链表、相交链表、链表的中间节点、回文链表)

文章目录 一、反转链表1、程序详解2、代码 二、相交链表1、程序详解2、代码 三、链表的中间节点1、程序详解2、代码 四、回文链表1、程序详解2、代码 一、反转链表 1、程序详解 题目:给定单链表的头节点 head ,请反转链表,并返回反转后的链…

STM32实现硬件IIC通信(HAL库)

文章目录 一. 前言二. 关于IIC通信三. IIC通信过程四. STM32实现硬件IIC通信五. 关于硬件IIC的Bug 一. 前言 最近正在DIY一款智能电池,需要使用STM32F030F4P6和TI的电池管理芯片BQ40Z50进行SMBUS通信。SMBUS本质上就是IIC通信,项目用到STM32CubeMXHAL库…

基于ROS的智能网联车远程交互软件,全UI无需记忆指令,剑指核心原理。

基于ROS的智能网联车远程交互软件,全UI无需记忆指令,剑指核心原理。 服务于中汽恒泰,伟大的项目,希望看官点赞,谢谢~~ 进程(节点)列表化,参数面板化,实现快速机器人配置…