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洗地机哪家强?口碑洗地机推荐
现如今,智能家电在人们生活中变得越来越受欢迎,例如智能洗地机的出现,不仅省时省力,还实现了家务清洁的自由。在家庭中,地面清洁一直是一个令人头疼的问题,各种智能家居品牌通过开发各种智能家电产品来解决…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
vulnyx Blogger writeup
信息收集 arp-scan nmap 获取userFlag 上web看看 一个默认的页面,gobuster扫一下目录 可以看到扫出的目录中得到了一个有价值的目录/wordpress,说明目标所使用的cms是wordpress,访问http://192.168.43.213/wordpress/然后查看源码能看到 这…...
iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
