变量、作用域与内存

通过变量使用原始值与引用值

  1. 原始值
    • 最简单的数据,大小固定,保存在栈内存中,保存原始值的变量按值访问
    • 不能有属性,但尝试添加属性不报错
    • 使用new关键字,Javascript会创建一个Object类型的实例,但其行为类似原始值
    • 赋值到另一个变量时,是新增一个新的原始值
  2. 引用值
    • 由多个值构成的对象,存储在堆内存中,保存引用值的变量按引用访问
    • 可以随时增删改其属性和方法
    • 赋值到另一个变量时,只是复制了指针地址,没有新增一个对象

相同点:

  • 传递参数都是按值传递
    • 原始值,传原始值
    • 引用值,传指针地址

确认类型

  • typeof适合用来判断一个变量是否为原始类型(字符串、数值、布尔值或undefined)
  • instanceof来判断它是什么类型对象
    • 用instanceof检测原始值,则始终会返回false

在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。

理解执行上下文

执行上下文就是当前代码的执行环境,包括全局执行上下文(就是最外层的上下文),函数执行上下文

  • 全局执行上下文:创建一个全局的window对象(浏览器的情况下),并且设置this的值等于这个全局对象。
  • 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。

js引擎内部有一个执行上下文栈,它是用于执行代码的调用栈,最底部是全局执行上下文栈,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。

理解三个重要属性

  1. 变量对象

    每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。我们编写的代码无法访问变量对象

    活动对象

    在函数上下文中,我们用活动对象来表示变量对象,活动对象和变量对象其实是一个东西,只有当进入一个执行环境时,这个执行上下文的变量对象才会被激活,此时称为活动对象(AO),只有活动对象上的各种属性才能被访问。

    活动对象是在进入函数上下文时刻被创建的,最初只有一个定义变量:arguments(全局上下文中没有这个变量),它通过函数的arguments属性初始化。arguments属性值是Arguments对象(调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素。)

    变量对象和活动对象的关系:

    未进入执行阶段之前,变量对象(VO)中的属性都不能访问,但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

    它们其实都是同一个对象,只是处于执行上下文的不同生命周期

  2. 作用域链

    上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。

  3. this

执行上下文的生命周期

可以分为两个个阶段

  1. 创建阶段

    在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

    1. 创建变量对象
      • 初始化函数的所有形参
        • 由名称和对应值组成的一个变量对象的属性被创建
        • 没有实参,属性值设为undefined
      • 初始化函数声明
        • 由名称和对应值(function-object)组成一个变量对象的属性被创建
        • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
      • 初始变量声明
        • 由名称和对应值(undefined)组成一个变量对象的属性被创建
        • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
    2. 建立作用域
    3. 确定this指向
  2. 代码执行阶段

    创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

变量声明

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

标识符查找

搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。

理解垃圾回收

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

两种策略

标记清理

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

给变量加标记得两种实现思路

  • 当变量进入上下文时,反转某一位
  • 维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表

标记过程的实现并不重要,关键是策略

垃圾回收运行过程

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

引用计数

思路

  • 对每个值都记录它被引用的次数
  • 声明变量并给它赋一个引用值时,这个值的引用数为1
  • 如果同一个值又被赋给另一个变量,那么引用数加1
  • 如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1
  • 当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了
  • 释放引用数为0的值的内存

缺陷

会有可能出现循环引用,如:对象A有一个指针指向对象B,而对象B也引用了对象A

性能

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

现代垃圾回收程序会基于对JavaScript运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。

内存管理

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

优化内存方法

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

  1. 使用constlet声明

    块级作用域能更早地让垃圾回收程序介入,今早回收应该回收地内存

  2. 隐藏类和删除操作

    • V8会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。如果这实例共享同一个构造函数和原型,V8会在后台配置,共享相同的隐藏类
      • 后面如果某个实例新增新的属性,他们不会共享相同隐藏类,所以要避免JavaScript的“先创建再补充”式的动态属性赋值,并在构造函数中一次性声明所有属性
    • 动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果
  3. 静态分配(这个方法,大多数情况下,这都属于过早优化)

    关键点:减少浏览器执行垃圾回收的次数。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能

    浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。

    思路:

    1. 使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。

    2. 应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。

    3. 由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。

内存泄漏

JavaScript中的内存泄漏大部分是由不合理的引用导致的

可能出现的场景

  1. 定时器导致

    1
    2
    3
    4
    let name = "Jake";
    setInterval(() => {
    console.log(name);
    }, 100);

    只要定时器一直运行,回调函数中引用的name就会一直占用内存。因而垃圾回收程序就不会清理外部变量。

  2. 闭包

    1
    2
    3
    4
    5
    6
    let outer = function () {
    let name = "Jake";
    return function () {
    return name;
    };
    };

    闭包一直引用这外部函数的变量,垃圾回收程序无法去清理

Javascript代码执行过程

  1. 编译阶段
    1. 词法分析
    2. 语法分析
    3. 可执行代码生成
    4. 作用域确定
  2. 执行阶段
    1. 进入执行上下文声明周期
      1. 创建阶段
        1. 生成变量对象
          1. 创建argument对象
          2. 检查function函数声明
          3. 检查var变量声明
        2. 建立作用域链
        3. 确定this指向
      2. 执行阶段
        1. 变量赋值
        2. 函数引用
        3. 执行其他代码
    2. 代码执行
    3. 垃圾回收
      • 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数,全局上下文在应用程序退出前才会被销毁。