[JS红宝书笔记]变量、作用域与内存

804 阅读7分钟

序言

首先回顾一下上一章节:介绍了 JavaScript 的语法、变量声明、数据类型、操作符、语句还有函数。今天第四更底层的知识,做好笔记,开始。

MIND

原始值和引用值

ECMAScript 变量包括原始值引用值两种,有以下区别:

  • 构成:原始值就是最简单的数据,而引用值是多个值构成的对象
  • 访问:保存原始值的变量是按值访问,操作存储在变量中的实际值;保存引用值的变量是按引用访问,实际操作的是对该对象的引用
  • 动态属性:原始值不能有属性,而引用值有,而且可以动态添加、修改和删除属性(和方法)
  • 复制值:变量赋给另一个变量时,两者都是将变量中的值复制到新变量所在的位置,但原始值复制的值是值的副本,而引用值是指向存储在堆内存中的对象的指针

传递参数

无论原始值还是引用值,向函数传递参数时,都是按值传递,也就是说跟原始值和引用值的复制一样地传递方式-——将函数外的值复制到函数内部的参数。在 ECMAScript 中,函数的参数就是局部变量,因此,按值传递的值会被复制到一个局部变量。

确认类型

上一章节数据类型检测的操作符 typeof 用来检测原始值还行,可是遇到引用值就只会 "object" 这一套,连 null 都算进去。instanceof 就可以解决问题。

const colorList = ['green', 'yellow', 'cyan'];
console.log(colorList instanceof Array); // true

注意,用 instanceof 检测原始值返回 false

执行上下文

这是该章节最为重要的知识点,可能需要后面知识量贯通更好理解,这里仅仅讲述一些概念。

执行上下文(Execution Context),简称 EC

  • 来源:JS 引擎执行可执行代码前,进行代码解析,创建执行上下文
  • 含义:决定变量或函数可以访问哪些数据,还有决定它们的行为,是 JS 的执行环境
  • 分类:全局执行上下文、函数执行上下文、eval 执行上下文(很少使用)
  • 组成元素:变量对象(Variable object,VO)、作用域链(Scope chain)、this(后面章节介绍)
  • 管理:执行上下文栈(Execution context stack,ECS)

执行上下文栈

  • 全局执行上下文:最外层的上下文,同时也是执行上下文栈中最下层。任何不在函数内部的代码都会在全局上下文中
  • 每当函数被调用,都会为该函数创建一个新的上下文 用来管理执行上下文,当 JS 开始解释执行代码时,首先遇到全局执行代码,初始化时将对应的全局执行上下文押人员栈底,等待程序执行完毕后才会清空。程序执行中遇到函数执行代码,创建函数执行上下文,压入执行上下文栈,等待函数执行完毕,遵从栈的后进先出规则,函数执行上下文会从栈中弹出

变量对象

变量对象存放上下文中所定义的所有变量和函数,无法通过代码访问,用于后台处理数据。

全局对象

全局上下文中的变量对象就是全局对象,根据不同的宿主环境而产生差异,在浏览器中,全局对象是 window 对象。全局对象是预定义的对象,作为 JS 的全局函数和全局属性的占位符,通过使用全局对象可以访问所有其他预定义的对象、函数和属性。

活动对象

函数执行上下文中,将活动对象(activation object,AO)作为变量对象。活动对象是进入函数执行上下文时被创建的,通过函数的 arguments 属性初始化。本身与边来那个对象是一个东西,但不同的是,活动对象是激活的变量对象,该对象上的各种属性可以访问。

作用域链

作用域链决定了各级上下文中的代码在访问变量和函数的顺序,其本质是一个变量对象的指针列表,其中全局变量始终位于作用域链的最前端。当查找变量时,会从当前执行上下文的变量对象中查找,如果没有找到,就会从父上下文的变量对象中查找,直到找到全局变量。

作用域链增强

除了三种执行上下文外,可通过在作用域链前端临时添加上下文来增强作用域链,通常有两种方式:

  • try/catch 语句的 catch 语句
  • with 语句

推荐

这部分需要好好理解,推荐以下文章细细咀嚼:

作用域

作用域是指程序源代码中定义变量的区域。规定了如何查找变量,确定当前执行代码对变量的访问权限

变量提升

var 声明变量时,变量会自动添加到最接近的上下文,即全局上下文或函数上下文中,被拿到全局作用域或函数作用域的顶部,位于作用域中所有代码之前,形成变量提升

暂时性死区

let 声明变量其实也存在变量提升,但它形成的块级作用域有暂时性死区,不能在声明前使用 let 声明的变量,因此看起来变量没有提升。

垃圾回收

JS 通过自动内存管理实现内存分配和闲置资源回收。

  • 思路:确定不再使用的变量,然后释放该变量所占内存。
  • 缺陷:难以判定变量是否不再使用
  • 解决:采用标记策略跟踪变量是否还会使用,主要有:标记清理和引用计数

标记清理

垃圾回收时,首先标记内存中存储的所有变量,然后将所有在上下文中的变量,以及这些变量引用的变量的标记去除。最后垃圾回收程序进行内存清理,销毁带标记的所有值并收回它们的内存。

引用计数

不常用的策略。思路是对每个值都记录它被引用的次数,初始声明时次数为 1,该值随保存该值的变量而变动,当变量被引用赋值加 1,被其他值覆盖时减 1。如果引用值为 0,则在垃圾回收时释放该值内存

性能

垃圾回收程序的周期性运行会影响到程序的运行,造成性能损失。各个浏览器引擎会基于 JS 运行时环境的探测来决定何时运行,各有差异。其中 Chrome 的 V8 是根据活跃对象的数量增加一些余量,而 IE 是分配固定的数目,性能不堪。

内存管理

为了避免运行大量 JS 导致网页耗尽内存,浏览器所分配到的内存是限制住的。需要优化内存占用:

  • 解除引用:不需要的数据,设置为 null,等待释放
  • 声明优化:不使用 var,优先使用 const,然后是 let,后两者能更快进入到垃圾回收
  • 隐藏类:避免通过隐藏类动态的添加属性赋值,在构造函数中提前一次性声明
  • 避免内存泄漏:避免意外声明全局变量、定时器设有关闭语句、谨慎使用闭包
  • 静态分配和对象池:静态分配是极端形式,不考虑。对象池策略是创建对象池来管理可回收对象,不需要初始化对象,避开垃圾回收检测,因此不会频繁触发垃圾回收程序

总结

这一章笔记很空洞,emmmm,但都是很重要的内容,需要多看看其他文章,或者说红宝书其他章节都知晓的情况下更容易总结。所以,下一章:基本引用类型。