JavaScript基础 -- 变量、作用域与内存

1 原始值与引用值

原始值就是最简单的数据,引用值则是由多个值构成的对象。在把一个值赋给变量时,JavaScript引擎必须要确定这个值是原始值还是引用值
原始值大小固定,保存在内存上;引用值是对象,存储在内存上
它们存在以下不同:

  1. 动态属性

定义的方式很类似,都是创建一个变量再给它赋值,但在变量保存了这个值以后,对于引用值来说,可以随时添加、修改和删除其属性和方法;原始值不能有属性,尝试添加也不会报错

PS:原始类型的初始化可以只使用原始字面量形式,如果使用 new 关键字,JavaScript则会创建一个 Object 类型的实例

在这里插入图片描述

  1. 复制值

从一个变量到另一个变量复制原始值会创建该值的第二个副本,这两个变量可以独立使用,互不干扰

包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身;从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象


  1. 传递参数

ECMAScript中所有函数的参数都是按值传递的,函数外的值会被复制到函数内部的参数中,复制方式跟上面提到的规则一样

对象是按值传递的,比如下面这个例子:
在这里插入图片描述
如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 name 为 Greg 的对象,而 name 值是 Nicholas,这表明函数中参数的值改变之后,原始的引用没有变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针,而那个本地对象在函数执行结束时就被销毁了

PS:ECMAScript中函数的参数就是局部变量


  1. 确定类型

typeof操作符适合用来判断一个变量是否为原始类型,但值是对象或null,判断结果都是object

而对于引用值,我们通常不关心一个值是不是对象,而想知道是什么类型的对象,instanceof则来判断,如果变量是给定引用类型的实例,则返回true。按照定义,所有引用值都是object,因此检测任何引用值和 object 构造函数都会返回true

2 执行上下文与作用域

任何变量都存在于某个执行上下文中(也称为作用域),这个上下文决定了变量的生命周期,以及它们可以访问代码的哪些部分。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在这个对象上。执行上下文分全局上下文、函数上下文和块级上下文
1)全局上下文是最外层的上下文,在浏览器中,全局上下文就是 window 对象

2)每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上,ECMAScript程序的执行流就是通过这个上下文栈进行控制的

代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜素变量和函数

如果上下文是函数,则其活动对象用作变量对象,它最初只有一个定义变量:arguments。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文,以此类推直到全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象

此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量,比如下面这个例子:
在这里插入图片描述
下图展示了上面例子的作用域链:
在这里插入图片描述
内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文的任何东西,上下文之间的连接是线性的、有序的

PS:函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则

2.1 作用域链增强

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除,通常在下面这两种情况下会出现这个现象:

  • try/catch语句的 catch 块
    • 会创建一个新的变量对象,它会包含要抛出的错误对象的声明
  • with语句
    • 会向作用域链前端添加指定的对象

2.2 变量声明

除了 var 声明变量,ES6还增加了 let 和 const 关键字

  1. 使用 var 的函数作用域声明

在使用 var 声明变量时,变量会被自动添加到最接近的上下文,在函数中,最接近的上下文就是函数的局部上下文。如果变量未经声明就被初始化了,那它就会自动被添加到全局上下文

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这叫做"提升"


  1. 使用 let 的函数作用域声明

let 的作用域是块级的,块级作用域由最近的一对包含花括号{}界定。let 在同一作用域内不能声明两次,重复的 var 声明会被忽略,而重复的 let 声明会抛出SyntaxError

let 适合在循环中声明迭代变量,使用 var 声明的迭代变量会泄漏到循环外部


  1. 使用 const 的函数作用域声明

使用 const 声明的变量必须同时初始化为某个值,一经声明,在其生命周期的任何时候都不能再重新赋值,其他跟 let 声明一样

const 声明只应用到顶级原语或对象,换句话说,赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制;如果想让整个对象都不能修改,可以使用Object.freeze(),这样虽然不会报错但赋值会失败
在这里插入图片描述
在这里插入图片描述
由于 const 声明变量的值是单一类型且不可修改,JavaScript运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找,谷歌的 V8 引擎就执行这种优化

3 垃圾回收

JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存,通过自动内存管理实现内存分配和闲置资源回收。基本思路:确定哪个变量不会再使用,就释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一段时间就会自动执行

以函数中局部变量的正常生命周期为例:函数中的局部变量会在函数执行时存在,栈(或堆)内存会分配空间以保存相应的值;函数在内部使用了变量,然后退出;此时不再需要那个局部变量了,它占用的内存可以释放。这种情况下显然不需要局部变量了,但并不是所有时候都这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用。标记未使用的变量的实现方式主要有标记清理和引用计数这两种标记策略

3.1 标记清理

JavaScript最常用的垃圾回收策略是标记清理。当变量进入上下文,会被加上存在于上下文中的标记;当离开上下文时,也会被加上离开上下文的标记。给变量加标记的方式有很多种,比如反转某一位,或者维护"在上下文中"和"不在上下文中"两个变量列表。标记过程的实现并不重要,关键是策略

垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后将所有在上下文中的变量,以及被在上下文中的变量引用的变量标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回自己的内存

3.2 引用计数

另一种没那么常用的垃圾回收策略是引用计数,其思路是对每个值都记录它被引用的次数,当一个值的引用数为 0 时,就说明没办法再访问到这个值了,垃圾回收程序下次运行的时候就会释放引用数为0的值的内存

这种策略存在循环引用的问题,objectA 和 objectB 通过各自的属性相互引用,引用数永远不会是0,如果函数被多次调用,则会导致大量内存永远不会被释放
在这里插入图片描述

3.3 性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。开发时不知道什么时候运行时会收集垃圾,最好的办法是在写代码时要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行,探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的

3.4 内存管理

JavaScript基于安全考虑,分配给浏览器的内存通常比分配给桌面软件的少很多,为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量

优化内存占用的最佳手段是保证在执行代码时只保存必要的数据,如果数据不再必要,就将其设置为null,从而释放其引用,这也叫解除引用。这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用,如下面例子所示:
在这里插入图片描述
localPerson 在 createPerson() 执行完成超出上下文后会自动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其引用,最后一行就是这么做的

PS:解除对一个值的引用并不会自动导致相关内存被回收,解除引用的关键在于确保相关的值已经不在上下文里了,因此在下次垃圾回收时会被回收

  1. 通过 let 和 const 提升性能

let 和 const都以块(而非函数)为作用域,所以相比于使用var,这两个可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存


  1. 隐藏类和删除操作

根据 JavaScript 所在的运行环境,有时需要根据浏览器使用的引擎来采取不同的性能优化策略。Chrome使用 V8 Javascript 引擎,V8在将解释后的 JavaScript 代码编译为实际的机器码时会利用"隐藏类",这对代码性能有一定的影响

运行期间,V8会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征,能够共享相同隐藏类的对象性能会更好,比如下面这段代码:
在这里插入图片描述
V8会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。假如之后添加a2.auther = 'Jake'这段代码,此时两个实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这可能对性能产生明显影响

解决方案就是避免 JavaScript 的"先创建再补充"式的动态属性赋值,并在构造函数中一次性声明所有属性
在这里插入图片描述
这样两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类,从而带来潜在的性能提升。不过,使用 delete 关键字会导致生成相同的隐藏类片段
在这里插入图片描述
在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果是一样,最佳的做法是把不想要的属性设置为null,这样既可以保持隐藏类不变和继续共享,也能达到删除引用值供垃圾回收程序回收的效果


  1. 内存泄漏

JavaScript 中的内存泄漏大部分是由不合理的引用导致的
1)意外声明全局变量
没有使用任何关键字声明变量,解释器会把变量当作 window 的属性来创建,只要 window 本身不被清理就不会消失。只需要在变量声明前加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离开作用域

2)定时器
只要定时器一直运行,函数引用的变量就会一直占用内存,需要及时清除定时器

3)闭包
在这里插入图片描述
调用 outer() 会导致分配给 name 的内存被泄漏,只要返回的函数存在就不能清理name,因为闭包一直在引用着它。如果 name 的内容很大,那可能就是个大问题了


  1. 静态分配与对象池

为了提升性能,一个关键问题就是如何减少浏览器执行垃圾回收的次数。我们无法直接控制什么时候收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能

浏览器决定何时运行垃圾回收程序的一个标准是对象更替的速度。如果有很多对象被初始化,然后又一下子超出了作用域,浏览器就会采用更激进的方式调度垃圾回收程序运行,这样就会影响性能
以下是个计算二维矢量加法的函数:
在这里插入图片描述
调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个矢量对象的生命周期很短,那就会很快失去所有对它的引用,成为可以被回收的值。如果该函数被频繁调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而就会更频繁安排垃圾回收

该问题的解决方案是不要动态创建矢量对象,比如修改上面的函数,让它使用一个已有的矢量对象:
在这里插入图片描述
这就需要在其他地方实例化矢量参数resultant,那在哪里创建矢量才不会让垃圾回收调度程序盯上呢?

一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替

如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择,不过需要留意不要招致额外的垃圾回收
在这里插入图片描述
由于 JavaScript 数组的大小是动态可变的,引擎会删除大小为 100 的数组,再创建一个新的大小为 200 的数组。垃圾回收程序会看到这个删除操作,说不定会跑来收一次垃圾。要避免这种动态分配操作,可以在初始化时就创建一个大小够用的数组,不过必须事先想好这个数组有多大

PS:静态分配是优化的一种极端形式,如果应用程序被垃圾回收严重拖了后腿,可以利用它提升性能,但这种情况并不多见

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

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

相关文章

密钥登录服务器

1. 生成 SSH 密钥对 如果您还没有生成密钥对,可以使用以下命令生成: ssh-keygen 在 root 用户的家目录中生成了一个 .ssh 的隐藏目录,内含两个密钥文件:id_rsa 为私钥,id_rsa.pub 为公钥。 在提示时,您可…

nginx 部署前端vue项目

👨‍⚕ 主页: gis分享者 👨‍⚕ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕ 收录于专栏:前端工程师 文章目录 一、🍓什么是nginx?二、🍓nginx 部署…

三大行业案例:AI大模型+Agent实践全景

本文将从AI Agent和大模型的发展背景切入,结合51Talk、哈啰出行以及B站三个各具特色的行业案例,带你一窥事件驱动架构、RAG技术、人机协作流程,以及一整套行之有效的实操方法。具体包含内容有:51Talk如何让智能客服“主动进攻”&a…

HTML-CSS-常见标签与样式

目录 一. 央视新闻排版1.1 标题1.2 正文1.3 案例1.3.1 顶部导航栏1.3.2 flex布局1.3.3 表单标签1.3.4 表单项标签1.3.5 表格 1.3 课程总结 \quad 一. 央视新闻排版 \quad \quad 1.1 标题 \quad ALTp就是用AI快速生成 标题一共有6级 \quad 1.2 正文 \quad 定义视频 定义图片 样…

【信息安全设计】系统安全设计方案,系统安全保护设施设计实施方案(Word原件)

1.1 总体设计 1.1.1 设计原则 1.2 物理层安全 1.2.1 机房建设安全 1.2.2 电气安全特性 1.2.3 设备安全 1.2.4 介质安全措施 1.3 网络层安全 1.3.1 网络结构安全 1.3.2 划分子网络 1.3.3 异常流量管理 1.3.4 网络安全审计 1.3.5 网络访问控制 1.3.6 完整性检查 1.…

基于STP文件的智能比对系统:思通数科带来高效机械制造解决方案

在机械制造领域,设计图纸与实物之间的精准对比至关重要,传统的比对方式往往需要耗费大量时间且容易出现错误,导致生产效率低下并影响产品质量。为了解决这些问题推出了一套基于STP文件的智能比对系统,结合大模型技术,集…

【51项目】51单片机自制小霸王游戏机

视频演示效果: 纳新作品——小霸王游戏机 目录: 目录 视频演示效果: 目录: 前言: 一、连接方式: 1.1 控制引脚 1.2. 显示模块 1.3. 定时器 1.4. 游戏逻辑与硬件结合 1.5. 中断处理 二、源码分析&#xff1a…

SpringMVC(五)实现文件上传

目录 1.先导jar包 2.在Springmvc.xml配置文件上传解析器 3. 编写文件上传的html页面 4.在controller包中创建FileController.class文件 5.文件下载 6.重启服务器测试 1.先导jar包 <dependency><groupId>commons-fileupload</groupId><artifactId>…

LabVIEW声波谐振管自动化测量系统

开发了一种基于LabVIEW的声波谐振管自动化测量系统。该系统利用LabVIEW的强大功能&#xff0c;实现了对声波谐振频率的精确测量&#xff0c;提高了实验数据的采集效率和准确性。系统主要应用于物理教学和科研中&#xff0c;用于研究声波在谐振管中的传播特性。 项目背景 传统的…

docker Error response from daemon

问题 Error response from daemon: Get "https://index.docker.io/v1/search?qnginx&n25": read tcp 192.168.50.233:54354->54.198.86.24:443: read: connection reset by peer Unable to find image redis:latest locally docker: Error response from d…

PHP框架+gatewayworker实现在线1对1聊天--gatewayworker说明(2)

文章目录 gatewayworker使用说明onConnect 说明 gatewayworker使用说明 gatewayworker里只需要使用Applications\YourApp下的Events.php文件。 对文件的代码进行一下改造&#xff0c;如下&#xff0c;我们只需要用到onConnect方法&#xff0c;写法固定&#xff0c;其他方法都…

【Hadoop】Hadoop安全之Knox网关

目录 一、概述 2.1 knox介绍 2.2 版本信息 二、部署 三、验证Knox网关 3.1 Hdfs RESTFULL 3.2 HDFSUI 3.3 YARNUI 3.4 HBASEUI 一、概述 2.1 knox介绍 Apache Knox网关是一个用于与Apache Hadoop部署的REST api和ui交互的应用程序网关。Knox网关为所有与Apache Hadoop…

PyCharm专项训练5 最短路径算法

一、实验目的 本文的实验目的是通过编程实践&#xff0c;掌握并应用Dijkstra&#xff08;迪杰斯特拉&#xff09;算法和Floyd&#xff08;弗洛伊德&#xff09;算法来解决图论中的最短路径问题。 二、实验内容 数据准备&#xff1a; 使用邻接表的形式定义两个图graph_dijkstra…

分布式算法(五):初识ZAB协议

文章目录 一、什么是Zookeeper二、ZAB与Zookeeper的关系为什么Zookeeper不直接使用Paxos 三、ZAB简介1.名词解释提案&#xff08;Proposal&#xff09;事务&#xff08;Transaction&#xff09;原子广播&#xff08;Atomic Broadcast&#xff09; 2.集群角色领导者&#xff08;…

word中插入zotero引用

1、参考文献末尾没有文献&#xff1f; 在文献条目要显示的地方点击“refresh” 2、参考文献条目没有悬挂缩进&#xff1f; 把“书目”添加到样式库中&#xff0c;修改样式为悬挂缩进1.5字符 3、交叉引用&#xff1f; 宏 新建一个宏 粘贴下面代码 Public Sub ZoteroLinkCita…

利用3DGS中convert.py处理自采数据

前言 3DGS源码中convert.py提供对自采数据集的处理&#xff0c;需要预先安装Colmap和ImageMagick. ubuntu22.04安装colmap 点击进入NVIDIA官网&#xff0c;查看GPU的CMAKE_CUDA_ARCHITECTURES 1、克隆colmap源码&#xff0c;并进入colmap文件夹 git clone https://github.c…

【Vue】vue-router使用addRoute动态加载路由后刷新页面404

场景&#xff1a;动态加载路由&#xff0c;点击菜单路由跳转正常&#xff0c;但刷新页面报404 原因&#xff1a;使用404做异常路由捕获 刷新页面会导致路由丢失&#xff0c;重建路由时先加载了静态路由&#xff08;包含异常路由捕获404&#xff09;&#xff0c;此时动态路由还未…

USB射频微波功率计的功能与优势-盛铂科技

USB射频功率计是一种用于测量射频信号&#xff08;RF&#xff09;功率的仪器&#xff0c;它通过USB接口与计算机或其他设备连接&#xff0c;以便于进行数据采集、处理和显示。 主要功能 功率测量&#xff1a;能够测量射频信号的功率&#xff0c;通常以毫瓦&#xff08;mW&…

【Vim Masterclass 笔记01】Section 1:Course Overview + Section 2:Vim Quickstart

文章目录 Section 1&#xff1a;Course Introduction 课程概述S01L01 Course Overview 课程简介课程概要 S01L02 Course Download 课程资源下载S01L03 What Vim Is and Why You Should Learn It 何为 Vim&#xff1f;学来干啥&#xff1f;1 何为 Vim2 为何学 Vim Section 2&…

Elasticsearch JavaRestClient版

文章目录 初始化RestHighLeveClient&#xff08;必要条件&#xff09;索引库操作1.创建索引库&#xff08;4步&#xff09;2.删除索引库&#xff08;3步&#xff09;3.判断索引库是否存在&#xff08;3步&#xff09;4.总结&#xff1a;四步走 文档操作1.创建文档&#xff08;4…