【js高级程序】变量、作用域与内存

红宝书学习记录

原文整理摘抄自 javascript 高级程序开发(第4版) 第4章

原始值与引用值

原始值 就是最简单的数据

目前有六种原始值 Undefined、Null、Boolean、Number、String 和 Symbol、BigInt

引用值 由多个值构成的对象 引用值是保存在内存中的对象。 JavaScript 不允许直接访问内存位置,因此也就 不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非 实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。 对于引用值而言,可以随时添加、修改和删除其属性和方法。

复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。

  • 原始值的存储复制完全独立,互不干扰 值复制
  • 引用值存储在变量中的值也会被复制到新变量所在的位置。区 别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象引用复制

传递参数

ECMAScript 中所有函数的参数都是按值传递的。 这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。

  • 如果是原始值,那么就跟原始值变量的复制一样
  • 如果是 引用值,那么就跟引用值变量的复制一样。

确定类型

typeof 确定值是不是对象 instanceof 反对元素是否是某个类型的实例

1
result = variable instanceof constructor
1
2
3
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

执行上下文与作用域

变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。

全局上下文是最外层的上下文。

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

每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。 类似洋葱模型

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。

1
2
3
4
5
6
7
8
9
10
var color = "blue";

function changeColor() {
  if (color === "blue") {
    color = "red";
  } else {
    color = "blue";
  }
}
changeColor();

函数 changeColor()的作用域链包含两个对象

  • 它自己的变量对象(就 是定义 arguments 对象的那个)
  • 全局上下文的变量对象 这个函数内部之所以能够访问变量 color,就是因为可以在作用域链中找到它

此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量。

作用域链增强

虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。 某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除

  • try/catch 语句的 catch 块 (创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明)
  • with 语句 (会向作用域链前端添加指定的对象) 这两种情况下,都会在作用域链前端添加一个变量对象。
1
2
3
4
5
6
7
8
function buildUrl() {
  let qs = "?debug=true";
  
  with(location) {
    let url = href + qs;
    return url;
  }
}

这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。当 with 语句中的代码引用变量 href 时,实际上引用的是 location.href,也就是自己变量对象的属性。

变量声明

var, let, const

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

    var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升” (hoisting)。

  1. 使用 let 的块级作用域声明 ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。 块级作用域由最近的一对包含花括号{}界定。 换句话说,if 块、while 块、function 块,甚至连单独 的块也是 let 声明变量的作用域。

    严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的 缘故,实际上不能在声明之前使用 let 变量。

    1
    2
    3
    4
    5
    for (let i = 0; i < 10; i ++) {
      setTimeout(() => {
        console.info(i);
      })
    }

    每次循环所生成的块级作用域包含着当时i的值, 块级作用域在定时器回调中被使用到,函数执行每次都有一个单独的作用域 i,可正常输出 0,2,3..9

  2. 使用 const 的常量声明 使用 const 声明的变量必须同时初始化为某个值。

1
const INIT_SIZE = 3;

垃圾回收

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。

基本思路:确定哪个变量不会再 使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执 行过程中某个预定的收集时间)就会自动运行。

回收策略: 标记清理

JavaScript 最常用的垃圾回收策略是标记清理

当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。当变量离开上下文时,也会被加上离开上下文的标记。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。

然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。

在此之后再被加上标记 的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

回收策略: 引用计数

另一种没那么常用的垃圾回收策略是引用计数

其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。

类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。

垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。

会存在循环引用问题

性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的 时间调度很重要。

在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。

内存管理

  • 解除引用: 如果数据不需要,把它设置null

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

  • 通过 const 和 let 声明提升性能 const let 都是以块(非函数)为作用域

  • 隐藏类和删除操作 运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。

    1
    2
    3
    4
    5
    6
    function Article() {
      this.title = 'Inauguration Ceremony Features Kazoo Band';
    }
    
    let a1 = new Article();
    let a2 = new Article();

    V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原 型。假设之后又添加了下面这行代码:

    1
    a2.author = 'Jake';

    此时两个 Article 实例就会对应两个不同的隐藏类。

    1
    2
    3
    4
    5
    6
    7
    function Article(opt_author) {
      this.title = 'Inauguration Ceremony Features Kazoo Band';
      this.author = opt_author;
    }
    
    let a1 = new Article();
    let a2 = new Article('Jake');

    这样,两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类, 从而带来潜在的性能提升。 不过要记住,使用 delete 关键字会导致生成相同的隐藏类片段。解决方式就是不删除该属性而是设置属性为null

  • 静态分配与对象池 (间接控制触发垃圾回收的条件)

小结

  • JavaScript 变量可以保存两种类型的值:原始值和引用值。
  • 原始值: UndefinedNullBooleanNumberStringSymbol
  • 原始值大小固定,因此保存在栈内存上
  • 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
  • 引用值是对象,存储在堆内存上
  • 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身
  • 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
  • typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。
  • 执行上下文分
    • 全局上下文
    • 函数上下文
    • 块级上下文。
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃 至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
  • 变量的执行上下文用于确定什么时候释放内存
  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算 法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对 象(如 DOM 元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。
✏️ 如有问题,欢迎指正
上一篇 : 【js高级程序】工作者线程-ServiceWorker下一篇 : 【js高级程序】BOM