卖坚果的怪叔叔 卖坚果的怪叔叔
  • 首页
  • 生活
  • 技术
  • 记录
  • 工具箱
  • 关于我
首页 › 技术 › 重学JavaScript之变量、作用域和内存问题

重学JavaScript之变量、作用域和内存问题

坚果大叔
2019-07-16 17:08:52技术阅读 3,526

重学JavaScript之变量、作用域和内存问题-卖坚果的怪叔叔

ES变量包含两种不同数据类型的值:基本类型值和引用类型值。

将一个值赋给变量的时候,解析器必须确定这个值是基本类型还是引用类型

1.1 基本类型值

指的是那些保存在栈内存中的简单数据,即这种值完全保存在内存中的一个位置。一下五种为基本类型值,它们在内存中分别占有固定大小的空间,因此它们的值可以保存在栈内存中。这样也可以提高查询变量的速度。对于保存基本类型值的变量,它们是按值访问的,我们操作的是它们实际的值。

  • Undefined
  • Null
  • Boolean
  • Number
  • String

1.2 引用类型值

指的那些保存在堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

因此如果想要给一个变量赋值一个引用类型的值,那么则必须在堆内存中为这个值分配空间,由于这种值的大小不固定,因此不能把她保存到栈内存中,但是内存地址的大小是固定的,因此可以将内存地址保存在栈内存中。这样,在查询引用类型的变量时,就可以首先从栈中读取内存地址,在找到在堆内存中保存的值。这种访问方式叫做引用访问,因为我们不是操作的实际的值,而是被那个值引用的对象。

1.3 动态属性

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。但是,当这个值保存到变量中以后,对不同类型值可以执行的操作则大相径庭。对于引用类型的值,我们可以为其添加属性和方法。也可以改变和删除其属性和方法。

var p = new Object()
p.name = 'vue'
alert(p.name)   // vue

1.4 复制变量值

除了保存方式不同之外,从一个变量向另外一个变量复制基本类型值和引用类型值时,也存在不同。如果从一个变量向另一个变量复制基本类型的值,会在栈中创建一个新值,然后把该值复制到为新变量分配的位置上:

var n = 5
var n2 = n

// 注意n 和 n2 之间是独立的。改变n 不会 改变 n2

当一个变量向另一个变量复制引用类型的值时,同样也会将存储在栈中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上是引用的同一个对象。因此改变其中一个另外一个也会改变。

var obj = new Object()
var obj2 = obj1
obj1.name = 'vue'
alert(obj2.name)    // vue

1.5 传递参数

ES中 所有函数的参数都是按值传递的,也就是说,把函数外部的值 复制给函数内部的参数。

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者就是arguments对象中的一个元素),在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。

function addTen(num){
    num += 10
    return num
}

var count = 20
var result = addTen(count)
console.log(count) // 20
console.log(result) // 30

1.6 检测类型

可以通过 typeof 操作符检测,在第三章中已经介绍过。当然typeof对基本数据类型的检测是很好,但是对于引用类型的检测则有点困难,这时候就可以通过 instanceof 来检测

result = variable instanceof constructor

如果变量是给定引用类型的实例,那么 instanceof 操作符就会返回true,如下:

person instanceof Object    // 变量 person 是 Object?

根据规定,所有引用类型的值都是Object的实例,因此,在检测一个引用类型值和Object构造函数时,instanceof 操作符始终会返回 true,如果使用 instanceof 操作符检测基本类型的值,则该操作符始终返回 false,因为基本类型不是对象。

注意: 使用 typeof 操作符检测函数时,该操作符会返回 function 。在 safari 和 chrome 中使用 typeof 检测正则表达式时,这个操作符会错误地返回 function

1.7 执行环境及作用域

执行环境是js中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为,每个执行环境都有一个与之关联的变量对象。环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境是最外围的执行环境,根据 ES 实现所在的宿主环境不同,表示执行环境的对象也不一样,在浏览器中,全局执行环境是 window 对象。因此所有全局变量和函数都是作为 window 对象的属性和方法创建的,某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。

每个函数在被调用时都会创建自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建由变量对象构成的一个作用域链,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象子最开始时只包含一个变量,即 arguments对象。作用域链中的下一个变量对象来在包含环境,而在下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析沿着作用域一级一级地搜索标识符的过程,搜索过程中始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符位置,如果找不到就报错。

重点: 在一个特定的执行环境中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的,每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另外一个执行环境。函数的参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。

1.7.1 延长作用域链

虽然执行环境的类型总共只有两种---全局和局部(函数),我们可以通过其他办法来延长作用域链。原理就是在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被一处。可以通过以下两种方式来实现:

try-catch语句的catch块
with语句

这个两个语句都会在作用域链的前端添加一个变量对象,对 with 语句来说,其变量对象中包含着为指定对象的所有属性和方法所作变量的声明。对catch 语句来说,其变量对象中包含的事被抛出的错误对象的声明。这些变量都是只读的,因此在 with 和 catch 语句中声明的变量都会被添加到所执行环境的变量对象中。

注意: 在IE的JavaScript实现中,存在一个与标准不一致的地方,即在 catch 语句中捕获的错误对象会被添加到执行环境的变量对象中。也就是说,即使在catch块的外部也可以访问到错误对象。

1.8 没有块级作用域

JavaScript 没有块级作用域。至少在当前这个版本没有

if (true) {
    var color = 'blue'
}

console.log(color)  // blue  

// 如果是其他语言 在 {} 之内的定义  在{} 外部是无法获取的

1.8.1 声明变量

在使用 var 关键字声明变量时,这个变量将被自动添加到距离最近的可用环境中,对于函数而言,这个最近的环境就是函数的局部环境,对于前面例子中 with 语句而言,这个最近的环境也是函数的环境。如果变量未经声明的情况下被初始化,那么该变量会被自动添加到全局环境。

注意: 在编写 JavaScript 代码的过程中,不声明而直接初始化变量时一个常见的错误做法,因为这样可能会导致意外,我们建议时在初始化变量之前,一定要先声明。

1.8.2 查询标识符

当在某个环境中为了读取和写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么,搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止、变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域向上搜索。搜索过程一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个标识符,那么则意味着该变量未声明。

var color = 'blue'
function getColor () {
    return color
}

alert(getColor()) // 'blue'

上述例子, 在调用函数 getColor() 时 会引用变量color,为了确定变量color的值,将开始一个两步搜索过程。首先,搜索 getColor()的变量对象,19查找其中是否包含一个名为color的标识符,在没有找到的情况下,搜索继续到下一个变量对象(全局环境的变量对象),然后在哪里找到了名为 color的标识符。因为搜索到了定义这个变量的变量对象,那么搜索过程结束。

注意: 如果在搜索过程中,如果存在一个局部的变量的定义,则搜索会自动停止,不在进入另一个变量对象。换句话说,如果局部环境中有同名标识符,就不会使用位于父环境中的标识符。

注意: 另外变量查询也是有代价的,访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。

1.9 垃圾收集

JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。它的原理是:找出那些不再继续使用的变量,然后释放其占用的内存。为此垃圾收集器会按照固定的时间间隔,周期性的执行这一操作。

1.9.1 标记清除

JavaScript 最常用的垃圾收集方式是标记清除,当变量进入环境时,就为这个变量标记一个 “进入环境”,从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要进入了环境,那么就代表可能会用到它,当变量离开环境的时候,会被标记上“离开环境”。垃圾收集器运行的时候会给存储在内存中所有的变量加上标记,然后去掉环境中的变量已经被环境中变量引用的变量标记,那么剩下来的就是准备删除的变量。最后垃圾收集器完成内存清除工作。

1.9.2 引用计数

还有种垃圾收集策略叫做引用计数,就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另一个变量则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1.当这个值的引用次数变成 0 时。则说明没有办法在访问这个值了,就可以将其占用的内存空间收回。

1.9.3 性能问题

垃圾收集器时周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。所有就导致性能问题。

1.9.4 管理内存

由于JavaScript分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少,这样做的目的时出于安全方面的考虑。目的是防止运行 JavaScript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。

因此,确保占用最少的内存可以让页面获得更好的新能,而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用---- 这个做法叫做 解除引用。这个方法适用大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境后自动被解除。

注意: 解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行的时候将其回收。

JavaScript变量可以用来保存两种类型的值:基本类型和引用类型。基本类型包括: Undefined、Null、Boolean、Number、String。它们具有以下的特点。

  1. 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中。
  2. 从一个变量向另一个变量赋值基本类型的值,会创建这个值的一个副本。
  3. 包含引用类型值的变量实际上包含的并不是对象本身,而是指向该对象的指针。
  4. 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象。
  5. 确定一个值是哪种基本类型可以使用 typeof操作符,而确定一个值是哪种引用类型可以使用 instanceof操作符。

所有变量都存在一个执行环境当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。如下:

  1. 执行环境有 全局执行环境 和 函数执行环境之分。
  2. 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链。
  3. 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境。
  4. 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据
  5. 变量的执行环境有助于确定应该何时释放内存

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题,如下:

  1. 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  2. “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  3. 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法,但再IE中访问非原生JavaScript对象时,这种算法仍然可能会导致问题。
  4. 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
  5. 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象,全局对象属性以及循环引用变量的引用。

JavaScript
赞赏 赞(0)
本文系作者 @坚果大叔 原创发布在 卖坚果的怪叔叔。未经许可,禁止转载。
Python获取好友地区分布及好友性别分布
上一篇
周末小记
下一篇
在小程序中查看

这是个😊,挣点服务器费用😜

热门文章
嗦鸭头~
三度胃镜有感
Flex布局入门
夏天到了
门前的空地,转眼又绿了
常用的JS 时间转换相关代码!
专题标签
agent (1) CSS (15) dom (1) Es6入门系列 (14) Git (3) html (1) iview (1) JavaScript (9) js (4) Lif (1) Node (4) pyecharts (1) Python (10) ref (1) scriptable (1) Skill (1) vsCode (1) vue (4) wordpress (2) 三月 (1) 上海 (1) 假期 (1) 其他分享 (3) 前端两三问 (15) 古镇 (1) 外滩 (1) 对象 (1) 小程序 (5) 年度总结 (6) 年终总结 (1) 微信机器人 (4) 打字机 (1) 技术 (1) 日常问题 (8) 日常问题记录 (1) 春天 (2) 服务器 (2) 海族馆 (1) 生活 (8) 网上国网 (1) 记录 (3) 通勤 (1) 重学JavaScript (11) 面试题 (10) 🌸 (1)
什么是Agent?
2026-08-15 23:49:12
499 0 2
什么是 Skill?
2026-08-12 21:32:42
424 1 0
失业在家,我给小朋友做了一个练字小程序!
2026-08-07 16:21:52
848 10 1
上线8年,15万用户的小程序,一共赚了多少钱?
2026-05-30 18:01:24
4,819 4 0
  • 0
博主

一位佛系的前端开发者,略通摄影,乐于尝试新事物,热衷于美食。

友链
故事胶片
公众号
坚果大叔 执行上下文 卖坚果的怪叔叔 Dacking
Copyright © 2017-2026 卖坚果的怪叔叔

开往-友链接力

萌ICP备20230818号

苏ICP备18048410号-3
  • 首页
  • 生活
  • 技术
  • 记录
  • 工具箱
  • 关于我
# CSS # # JavaScript # # vue # # 微信 # # 生活 #
坚果大叔
345
文章
171
评论
432
喜欢