《JavaScript 高级程序与设计》 | 变量、作用域与内存

35 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

4.1 原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值(栈)和引用值(堆)

原始数据类型和引用数据类型

  • :原始数据类型(UndefinedNullBooleanNumberStringSymbolBigInt
  • :引用数据类型(对象数组函数

只有引用值可以动态添加后面可以使用的属性。

注意:ECMAScript 中函数的参数就是局部变量。

typeof 操作符最适合用来判断一个变量是否为原始类型。它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。如果值是对象或 null,那么 typeof 返回 "object"

typeof 虽然对原始值很有用,但它对引用值的用处不大

ECMAScript 提供了 instanceof 操作符,通过 instanceof 操作符检测任何引用值和Object 构造函数都会返回 true

如果用 instanceof 检测原始值,则始终会返回 false,因为原始值不是对象。

4.2 执行上下文与作用域

执行上下文


1. 全局执行上下文

在浏览器中,全局上下文就是我们常说的 window 对象,所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 letconst 的顶级声明不会定义在全局上下文中。

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

一个程序中只有一个全局执行上下文。

2. 函数执行上下文

每个函数调用都有自己的上下文。当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

3. eval 执行函数上下文

eval() 调用内部存在第三种上下文,eval() 函数不常用,不做介绍。

变量声明


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

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

2. 使用 let 的块级作用域声明

块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。

3. 使用 const 的常量声明

除了 letES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

注意:赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制

如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败

4. 标识符查找

如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。

这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未明。

在局部变量声明之后的任何代码都无法访问全局变量,除非使用完全限定的写法 window.变量。

4.3 垃圾回收

通过自动内存管理实现内存分配闲置资源回收。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。

垃圾回收的两种主要的标记策略:标记清理引用计数

标记清理


最常用的垃圾回收策略是标记清理。标记过程的实现并不重要,关键是策略。

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

引用计数


引用计数没那么常用。其思路是对每个值都记录它被引用的次数。

声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存

存在循环引用的问题,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。**通过各自的属性相互引用,它们的引用数永远不会变成 0。**如果函数被多次调用,则会导致大量内存永远不会被释放。

为了补救这一点,IE9BOMDOM 对象都改成了 JavaScript 对象,这同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。

性能


垃圾回收程序会周期性运行,垃圾回收的时间调度很重要

垃圾回收有可能会明显拖慢渲染的速度和帧速率

只要满足其中某个条件,垃圾回收程序就会运行。

IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。

在某些浏览器中是有可能(但不推荐)主动触发垃圾回收的。在 IE 中,window.CollectGarbage()方法会立即触发垃圾回收。在 Opera 7 及更高版本中,调用 window.opera.collect()也会启动垃圾回收程序。

内存管理


优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性

  1. 通过 constlet 声明提升性能:有助于改善代码风格,有助于改进垃圾回收的过程

  2. 隐藏类和删除操作

  3. 内存泄漏

    意外声明全局变量是最常见但也最容易修复的内存泄漏问题。没有使用任何关键字声明变量;

    定时器也可能会悄悄地导致内存泄漏;

    闭包很容易在不知不觉间造成内存泄漏;

  4. 静态分配与对象池