JS作用域链和闭包
JS作用域链和闭包
- 引题
- 作用域链
- 词法作用域
- 闭包
- 思考题
- 闭包如何回收
引题
有没有人跟我一样,面试中要是问基础,最怕遇到的就是闭包问题,闭包在 JavaScript 中几乎无处不在,理解作用域链是理解闭包的基础,同时作用域链和作用域还是所有编程语言的基础。
首先来看一段示例代码:
function bar() {console.log(myname)
}
function foo() {var myname = 'yy'bar()
}
var myname = 'qq'
foo()
如果用调用栈的方式来描述这个执行过程,可以参考下图:

如果你看过调用栈你的第一反应可能是按照调用栈的顺序来查找变量:
先查找栈顶是否存在 myname 变量,如果没有就往下查找 foo 函数中的变量,找到了所有返回 yy。
但如果你运行这段代码就会知道,实际并非如此,最终输出结果其实是 qq,为何会是这种情况呢?要解释清楚这个问题,就需要先搞清楚作用域链。
作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用成为 outer。

当一段代码使用一个变量时,JavaScript 引擎首先会在当前执行上下文查找该变量,如果找不到就会继续在 outer 所指向的执行上下文中查找。图中可以看出,函数 foo 和 bar 的 outer 都指向全局执行上下文,所以 bar 函数中找不到变量 myname 时,下一步就是去全局执行上下文中找,结果为 qq。我们将这个查找的链条称为作用域链。
不过还有一个疑问,bar 函数是在 foo 函数中被调用的,为何它的外部引用 outer 不是 foo函数执行上下文却是全局执行作用域呢?要回答这个问题,还需要知道词法作用域,因为在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域
词法作用域是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态作用域。结合图就能更好的理解这句话:

从图中可以看出,main 函数包含了 bar 函数,bar 函数中包含了 foo 函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:
foo 函数作用域——>bar 函数作用域——>main 函数作用域——>全局作用域
了解了词法作用域以及 JavaScript 的作用域链,再回过头来看上面的那个问题的答案就是:
因为根据词法作用域,foo 和 bar 函数在被声明时的位置决定了它们的上级作用域都是全局作用域,所以当 bar 函数使用了一个它自己没有定义的变量时,顺着它的作用域链往上找,就是全局作用域。
因此,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的是没有关系的。
闭包
了解了变量环境、词法环境和作用域链,接下来聊聊闭包可能你会更好的理解。先来看下面这段示例代码:
function foo() {var myname = 'yy'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('qq')
bar.getName()
console.log(bar.getName())
首先我们看当执行到 foo 函数内部 innerbar 这段代码时调用栈的情况,参考下图:

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 方法背的一个专属背包,无论在哪调用它们都会背着这个专属包,而且除了它们其他任何地方都无法访问这个专属背包,这个背包就被称为 foo 函数的闭包。
现在我们对闭包一个正式的定义:
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
那这些闭包是如何使用的呢?当执行 bar.setName('qq') 这个方法时,这段代码 JavaScript 引擎会沿着 当前执行上下文——>foo 函数闭包——>全局执行上下文 的顺序来查找 myname 变量,可以参考下图的调用栈状态图:

图中可以看出,setName 的执行上下文中没有 myname 变量,foo 函数闭包中包含了该变量,所以会修改闭包中的 myname 变量的值;同理,当调用 bar.getName 时,所访问的变量也是位于 foo 函数闭包中的。
所以执行的结果为:
// 1
// 1
// qq
Chrome的“开发者工具”中也可以看到闭包的情况

思考题
var bar = {myname: 'yy',printname: function() {console.log(myname)}
}
function foo() {let myname = 'qq'return bar.printname
}
let myname = 'out'
let _printname = foo()
_printname()
bar.printname()
执行 let _printname=foo() 这段代码的调用栈状态图可参考下图

_printname() 其实调用的就是 bar 对象的 printname 方法,而这个方法用到了 myname 变量,但是由于该函数作用域内并没有这个变量,根据词法作用域规则,它声明的位置决定了它的作用域链,它的上一个作用域就是全局作用域,它返回的 myname 变量就是全局作用域中的 myname,即输出结果为 out。
通过开发者工具查看

如果你觉得
bar.printname()应该返回的是yy,在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,但是 JavaScript 的 this 机制 可以帮你理解并了解如何获取对象内部属性。
闭包如何回收
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是一个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么就会回收这块内存。
所以,在使用闭包时,尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大,就尽量让它成为一个局部变量。
相关文章:
JS作用域链和闭包
JS作用域链和闭包 引题作用域链词法作用域闭包思考题 闭包如何回收 引题 有没有人跟我一样,面试中要是问基础,最怕遇到的就是闭包问题,闭包在 JavaScript 中几乎无处不在,理解作用域链是理解闭包的基础,同时作用域链和…...
【Spring实战】15 Logback
文章目录 1. 依赖2. 配置3. 打印日志4. 启动程序5. 验证6. 调整日志级别7. 代码详细总结 Spring 作为一个现代化的 Java 开发框架,提供了很多便利的功能,其中包括灵活而强大的日志记录。本文将介绍如何结合 Spring 和 Logback 配置和使用日志,…...
Stable Diffusion WebUI安装合成面部说话插件SadTalker
SadTalker可以根据一张图片、一段音频,合成面部说这段语音的视频。图片需要真人或者接近真人。 安装ffmpeg 下载地址: https://www.gyan.dev/ffmpeg/builds/ 下载ffmpeg-git-full.7z 后解压,将解压后的目录\bin添加到环境变量的Path中。 在…...
CSS 纵向顶部往下动画
<template><div class"container" mouseenter"startAnimation" mouseleave"stopAnimation"><!-- 旋方块 --><div class"box" :class"{ scale-up-ver-top: isAnimating }"><!-- 元素内容 -->&…...
科普:敏捷估算为什么用斐波那契数列
被一个同学问:敏捷估算为什么用斐波那契数列?有什么意义? 简单说说我自己的简介: 敏捷开发中使用斐波那契数列来估算的原因是,斐波那契数列可以用于估算任务的难度级别,并帮助团队预测完成任务所需的时间…...
HarmonyOS资源分类与访问
资源分类与访问 应用开发过程中,经常需要用到颜色、字体、间距、图片等资源,在不同的设备或配置中,这些资源的值可能不同。 应用资源:借助资源文件能力,开发者在应用中自定义资源,自行管理这些资源在不同…...
message: 没有找到可以构建的 NPM 包,请确认需要参与构建的 npm 都在 `miniprogra
第一步:修改 project.config.json 文件 "packNpmRelationList": [{"packageJsonPath": "./package.json","miniprogramNpmDistDir": "./miniprogram/"}], "packNpmManually": true 第二步:…...
基于C#的机械臂欧拉角与旋转矩阵转换
欧拉角概述 机器人末端执行器姿态描述方法主要有四种:旋转矩阵法、欧拉角法、等效轴角法和四元数法。所以,欧拉角是描述机械臂末端姿态的重要方法之一。 关于欧拉角的历史,由来已久,莱昂哈德欧拉用欧拉角来描述刚体在三维欧几里…...
【百度前端三面面试题】
在某乎看到的《百度前端三面面试题全部公开,三面的最后一个问题令我窒息》 其中下面三个问题没有给出答案,我虽然是前端出身,但也面试过一些人,大概分析一下这些问题。 面试中问这几个问题的目的是什么 ,怎么回答 上…...
【Java面试题】HTTP与 HTTPS 的区别
HTTP 与 HTTPS 的区别 : 主要体现在三个方面,分别是 信息传输安全、证书和身份验证 、连接方式 信息传输安全: HTTP 是超文本传输协议,HTTP下的信息是明文传输的,因此使用HTTP协议可能导致信息被截获或者第三方恶意…...
vue3 v-model语法糖
vue2 中父子组件数据同步 父→子 子→父 如何实现? v-model“count” 或者 xxx.sync“msg” v-model 语法糖 完整写法 :value“count” 和 input“count$event” xxx.sync 语法糖 完整写法 :xxx“msg” 和 update:xxx“msg$event” 现在:一个 v-mo…...
【k8s】deamonset文件和说明
目录 deamonset的相关命令 deamonset的定义 deamonset的使用场景 deamonset的例子 deamonset字段说明 serviceAccountName DaemonSet的结构及其各个部分的作用 deamonset的相关命令 #查看<name-space>空间内有哪些deamonset kubectl get DaemonSet -n <na…...
Zookeeper-Zookeeper特性与节点数据类型详解
1.Zookeeper介绍 ZooKeeper 是一个开源的分布式协调框架,是Apache Hadoop 的一个子项目,主要用来解决分布式集群中应用系统的一致性问题。Zookeeper 的设计目标是将那些复杂目容易出错的分布式一致性服务封装起来,构成一高效可靠的原语集&…...
云计算复习提纲
第一章 大数据的概念:海量数据的规模巨大到无法通过目前主流的计算机系统在合理时间内获取、存储、管理、处理并提炼以帮助使用者决策 大数据的特点:①数据量大,存储的数据量巨大,PB级别是常态;②多样,数…...
Vue-响应式数据
一、ref创建基本类型的响应式数据 vue3可以使用ref、reactive去定义响应式数数据。 知识点汇总 使用ref需要先引入ref,import {ref} from vue在模板 template 中使用了添加ref 的响应式数据,变量的后面不用添加.value所有js代码里面,去操作r…...
Vue开发者必备!手把手教你实现类似Element Plus的全局提示组件!
前言 在Web开发中,用户体验至关重要。有效的信息提示和错误消息对于确保用户更好地理解和操作至关重要。在这个背景下,全局弹框提示组件成为了一个非常有用的工具。Vue.js,作为当前最受欢迎的前端框架之一,为创建灵活、可复用的弹…...
大数据 - Hadoop系列《三》- HDFS(分布式文件系统)概述
🐶5.1 hdfs的概念 HDFS分布式文件系统,全称为:Hadoop Distributed File System。 它是一个文件系统,用于存储文件,通过目录树来定位文件;其次,它是分布式的,由很多服务器联合起来实现其功能,集…...
Golang标准库sync的使用
Go语言作为现代编程语言,其并发编程的优势是有目共睹的。在实际编程中,我们常常需要保证多个goroutine之间的同步,这就需要使用到Go语言的sync标准库。sync库提供了基本的同步原语,例如互斥锁(Mutex)和等待…...
判断两张图片是否完全一致
判断两张图片是否为完全相同的图片 批量判断尺寸 大小 图像展示内容体是否完全一致的图片 import os import hashlib from PIL import Imagedef check_img_repeat(directory):"""批量对图片进行重复性校验是检查一组图像中是否有相同或几乎相同的图像副本。一…...
2024洗地机哪家强?口碑洗地机推荐
现如今,智能家电在人们生活中变得越来越受欢迎,例如智能洗地机的出现,不仅省时省力,还实现了家务清洁的自由。在家庭中,地面清洁一直是一个令人头疼的问题,各种智能家居品牌通过开发各种智能家电产品来解决…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
css3笔记 (1) 自用
outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size:0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格ÿ…...
C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看
文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...
