【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 = 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()

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

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

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

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

闭包

先看一段代码:

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这行代码时,调用栈如下图所示:

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.mzph.cn/pingmian/42578.shtml

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

相关文章

树形结构数据库存储表转换

有个树形的菜单, 当初设计表时将数据存储为 level0, level1,leve2,level3..., 表名menus_month 即0层级下子层级1,孙层级2 但是带来一个问题, 如何查询这个树形结构,变得非常复杂 以下是对数据表进行关系转换, 生成两张表, menus和 menus_relastionships 1.建菜单表及关系表…

nullptr和NULL

nullptr 既不是整型类型,也不是指针类型,nullptr 的类型是 std::nullptr_t(空指针类型),能转换成任意的指针类型。 NULL是被定义为0的常量,当遇到函数重载时,就会出现问题。避免歧义 函数重载…

如何在Spring Boot中实现OAuth2.0和OpenID Connect

如何在Spring Boot中实现OAuth2.0和OpenID Connect 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 一、OAuth2.0和OpenID Connect简介 OAuth2.0和OpenID Con…

数据结构--二叉树和堆

目录 1.基本概念 2.树的遍历方法 3.满二叉树&&完全二叉树 4.逻辑结构&&物理结构 5.推理公式 6.二叉树应用--堆 7.简单实现堆 1.基本概念 (1)这个里面的概念还是比较多的,但是大部分我们只需要了解即可,因为…

Ubuntu TensorRT安装

什么是TensorRT 一般的深度学习项目,训练时为了加快速度,会使用多 GPU 分布式训练。但在部署推理时,为了降低成本,往往使用单个 GPU 机器甚至嵌入式平台(比如 NVIDIA Jetson)进行部署,部署端也…

leetcode算法题总结

leetcode算法题总结 1、面试题整理1.1 入门1.2 基础1.3 适合1-3年面试1.4 适合3年以上面试 1、面试题整理 1.1 入门 1)两数之和(1, easy) 考察对map的使用,通过空间换时间 2)有效的括号(20, easy&#x…

Python的装饰器介绍

Python装饰器是一个强大的工具,可以增强函数或方法的功能而不改变它们的源代码。装饰器本质上是高阶函数(即接受一个函数作为参数的函数),它允许我们在函数的入口和出口添加逻辑,这使得代码更简洁、更具可维护性。下面…

VSCode使用ipynb文件高效地进行功能测试

一、ipynb是什么文件 .ipynb文件是Jupyter Notebook的专用格式,它允许用户在一个网页应用中混合编写Markdown文本、执行代码、查看输出结果及图表。Jupyter Notebook的本质是一个Web应用程序,支持运行40多种编程语言,包括Python。它的主要用…

java反射介绍

Java反射API允许你在运行时检查和修改程序的行为。这意味着你可以动态地创建对象、查看类的字段、方法和构造函数,甚至调用它们。这是一个强大的特性,但也应该谨慎使用,因为它可以破坏封装性。 以下是使用Java反射的一些常见用途:…

403 Forbidden:深入解析 HTTP 禁止访问错误及排查方法

目录 一、理解 403 Forbidden 错误的本质 二、系统化排查 403 Forbidden 错误 三、常见的解决方法 四、错误详尽排查指南 步骤1:掌握基本知识 步骤2:确定错误范围 步骤3:检查显而易见的问题 步骤4:核对权限和访问控制 步…

【鸿蒙学习笔记】MVVM模式

官方文档:MVVM模式 [Q&A] 什么是MVVM ArkUI采取MVVM Model View ViewModel模式。 Model层:存储数据和相关逻辑的模型。View层:在ArkUI中通常是Component装饰组件渲染的UI。ViewModel层:在ArkUI中,ViewModel是…

[AHK V2]获取本地IP地址

问题&#xff1a;如何用AutoHotkey v2 获取本地IP地址。 解答&#xff1a;AutoHotkey v2 源代码如下 #Requires AutoHotkey v2; MsgBox GetLocalIPByAdapter(Ethernet) ; <— specify the adapter name you are interested in ; MsgBox GetLocalIPByAdapter(以太网) ; <…

《算法笔记》总结No.4——散列

散列的英文名是hash&#xff0c;即我们常说的哈希~该知识点在王道408考研的教材里面属于查找的范围。即便各位并无深入了解过&#xff0c;也听说过散列是一种更高效的查找方法。 一.引例 先来考虑如下一个假设&#xff1a;设有数组M和N分别如下&#xff1a; M[10][1,2,3,4,5,6…

ARM/Linux嵌入式面经(十三):紫光同芯嵌入式

static关键字 static关键字一文搞懂这个知识点,真的是喜欢考!!! stm32启动时如何配置栈的地址 在STM32启动时配置栈的地址是一个关键步骤,这通常是在启动文件(如startup_stm32fxxx.s,其中xxx代表具体的STM32型号)中完成的。 面试者回答: STM32启动时配置栈的地址主…

java IO流(1)

一. 文件类 java中提供了一个File类来表示一个文件或目录(文件夹),并提供了一些方法可以操作该文件 1. 文件类的常用方法 File(String pathname)构造方法,里面传一个路径名,用来表示一个文件boolean canRead()判断文件是否是可读文件boolean canWrite()判断文件是否是可写文…

早起能给我带来了什么

现在我早已养成习惯&#xff0c;每天不用闹钟也能自然醒来。有时5点起&#xff0c;有时4点半起&#xff0c;最早4点起&#xff0c;通常不晚于5点半。 相比在7点左右起床&#xff0c;我每天多出了2小时&#xff0c;按一天8小时工作时长计算&#xff0c;每年可以多出约90个工作日…

Spring懒加载Bean机制

一、概述 默认情况下Spring容器启动时就会创建被它管理的Bean&#xff0c;但是有的时候被Spring管理的Bean并不需要再容器启动的时候被创建&#xff0c;而是当对象第一次被访问的时候进行创建&#xff0c;这种场景就可以使用懒加载实现。 也就是说&#xff1a;容器启动的时候…

现场Live震撼!OmAgent框架强势开源!行业应用已全面开花

第一个提出自动驾驶并进行研发的公司是Google&#xff0c;巧的是&#xff0c;它发布的Transformer模型也为今天的大模型发展奠定了基础。 自动驾驶已经完成从概念到现实的华丽转变&#xff0c;彻底重塑了传统驾车方式&#xff0c;而大模型行业正在经历的&#xff0c;恰如自动驾…

基于S32K144驱动NSD8381

文章目录 1.前言2.芯片介绍2.1 芯片简介2.2 硬件特性2.3 软件特性 3.测试环境3.1 工具3.2 架构 4.软件驱动4.1 SPI4.2 CTRL引脚4.3 寄存器4.4 双极性步进电机驱动流程 5.测试情况6.参考资料 1.前言 最近有些做电磁阀和调光大灯的客户需要寻找国产的双极性步进电机驱动&#xf…

Android 15 应用适配默认全屏的行为变更(Android V的新特性)

简介 Android V 上默认会使用全面屏兼容方式&#xff0c;影响应用显示&#xff0c;导致应用内跟导航标题重合&#xff0c;无法点击上移的内容。 默认情况下&#xff0c;如果应用以 Android 15&#xff08;API 级别 35&#xff09;为目标平台&#xff0c;在搭载 Android 15 的设…