JavaScript学习-4-变量、作用域与内存

215 阅读10分钟

JavaScript学习-4-变量、作用域与内存

4.1 原始值与引用值

ECMPScript变量可以包含两种不同类型的数据:原始值和引用值

  • 原始值就是最简单的数据:undefined、Null、Boolean、Number、String和Symbol,原始值是按值访问的,我们操作的就是存储在变量中的实际值
  • 引用值是由多个值构成的对象;在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身;保存引用值的变量是按引用访问的

4.1.1 动态属性

  • 原始值不能有属性,尽管尝试给原始值添加属性不会报错

4.1.2 复制值

  • 原始值复制之后相互独立

    let num1 = 5;
    let num2 = num1; // num1和num2相互独立,互不干扰
    
  • 引用值的复制实际上是指针的复制,两个指针指向了同一个对象,因此,通过两个引用修改对象会互相影响

4.1.3 传递参数

  • 所有函数的参数都是按值传递的
  • 函数的参数是局部变量

4.1.4 确定类型

  • typeof可以用于检测是否是function

  • typeof对原始值很有用,但是对引用值用处不大

  • 当需要知道是什么类型的对象时,需要使用instanceof操作符

    colors instanceof Array
    

4.2 执行上下文与作用域

  • 变量或函数的上下文决定了它们可以访问那些数据以及它们的行为
  • 每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上
  • 全局上下文:全局上下文是最外层的上下文,根据不同的宿主环境(浏览器),表示全局上下文的对象可能不一样(window对象)
  • 通过var定义的全局变量和函数都会成为window对象的属性和方法,但是,使用letconst的顶级声明不会定义在全局上下文中
  • 上下文的销毁:上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数;全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器
  • 每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文被推入一个上下文栈中;在函数执行完后,上下文栈会弹出该函数上下文,将控制权返回给之前的执行上下文
  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序
  • 代码正在执行的上下文的变量对象始终位于作用域链的最前端,如果上下文是函数,则其活动对象用作变量对象
  • 当查找变量或函数时,会沿着作用域链向上查找,直到找到全局上下文的变量对象停止;全局上下文的变量对象始终是作用域的最后一个变量对象

4.2.1 作用域链增强

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除,通常有两种情况会出现这个现象:

  • try/catch语句的catch语句:会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明
  • with语句:会向作用域链前端添加指定的对象

4.2.2 变量声明

4.2.2.1 使用var的函数作用域声明

  • 在使用var声明变量时,变量会被自动添加到最接近的上下文;在函数中,最接近的上下文就是函数的局部上下文

4.2.2.2 使用let的块级作用域

  • let的作用域是块级的
  • 块级作用域由最近的一对包含花括号{}界定;换句话说,if块,while块、function块,甚至连单独的块也是let声明变量的作用域
  • let在同一作用域内不能声明两次
  • let适合用于for循环遍历,使用var声明的迭代变量会泄露到循环外部
  • let变量不能在声明之前使用

4.2.2.3 使用const的常量声明

  • 推荐使用const

4.2.2.4 标识符查找

  • 沿着作用域链查找,直到查找到全局上下文的变量对象或者找到了标识符停止查找

4.3 垃圾回收

  • JavaScript通过自动管理实现内存分配和闲置资源回收
  • 基本思路:确定哪个变量不会再使用,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行
  • 如何判定哪个变量还有用,哪个变量不会再使用?标记未使用的变量主要有两种策略:标记清理引用计数

4.3.1 标记清理-推荐

  • 当变量进入上下文,比如在函数内部声明一个变量时,变量就会被加上 存在于 上下文的标记
  • 当变量离开上下文时,也会被加上离开上下文的标记
  • 给变量加标记的方式:反正某个标志位或者维护一个列表

4.3.2 引用计数

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

  • 如果保存对该值的引用的变量被其他值给覆盖了,那么引用数减1

  • 当一个值的引用数为0时,就说明没有办法再访问这个值,因此可以回收内存

  • 问题:循环引用,对象A有一个指针指向对象B,对象B也引用了对象A

    function Problem() {
      let objectA = new Object();
      let objectB = new Object();
      
      objectA.someOtherObject = ObjectB;
      objectB.anotherObject = ObjectA;
    }
    

    objectAobjectB的引用数都是2,当函数结束后,objectAobjectB都不在作用域中,但是根据引用计数的原则, objectAobjectB的引用计数变成1,并且永远不会变成0,objectAobjectB永远不会被回收,造成了内存泄露

4.3.3 性能

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

  • 在一次完整的垃圾回收之后,V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收
  • IE6:触发垃圾回收时候的变量个数、对象/数组字面量个数、数组槽位固定
  • IE7:动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值

4.3.4 内存管理

  • 如果数据不再必要,将其设置为null,释放其引用

4.3.4.1 通过constlet声明提高性能

  • constlet具有块级作用域,可能会更早的让垃圾回收
  • const > let > var

4.3.4.2 隐藏类和删除操作

  • V8引擎在将解释后的javascript代码编译为实际的机器码时会利用隐藏类

    function Article() {
      this.title = 'aaa';
    }
    
    let a1 = new Article();
    let a2 = new Article();
    

    此时,a1和a2这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型

    a2.author = 'Jake';
    

    如果添加了这行代码,此时a1和a2就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,可能会对性能产生影响

    解决方案:

    function Article(author) {
      this.title = "aaa";
      this.author = author;
    }
    
    let a1 = new Article();
    let a2 = new Article('Jake');
    

    此时可以共享一个隐藏类,从而带来潜在的性能提升。

  • 使用,delete关键字会导致生成相同的隐藏类片段

    function Article(author) {
      this.title = "aaa";
      this.author = author;
    }
    
    let a1 = new Article();
    let a2 = new Article('Jake');
    
    delete a2.author;
    

    此时,即使两个实例使用了同一个构造函数,他们也不再共享一个隐藏类。动态删除属性和动态添加属性导致的后果一样

    最佳实践:将不想要的属性设置为null,这样可以保持隐藏类不变和继续共享

4.3.4.3 内存泄露

  • 场景1: 变量声明未加关键字,提升为全局变量
function setName() {
    name = 'jake';
}

name变量会提升为全局变量,并且当做window的属性来创建。只要程序未关闭,变量就一直不会被回收

  • 场景2:定时器
let name = 'Jake';
setInterval(() => {
  console.log(name);
}, 100);

只要定时器一直运行,name变量就会一直占用内存

  • 场景3:闭包
let outer = function() {
  let name = 'Jake';
  return function() {
    return name;
  }
}

只要返回的函数存在就不能清理name,因为闭包一直在引用他,如果name的内容很大,就会一直占用很大的内存

4.3.4.4 静态分配与对象池

如何减少浏览器执行垃圾回收的次数?

  • 开发者无法直接控制什么时候开始收集垃圾,但是可以间接控制触发垃圾回收的条件
  • 理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那么就可以保住因释放内存而损失的性能
  • 浏览器决定何时进行垃圾回收程序的一个标准就是对象更替速度
  • 如果很多对象被初始化,然后一下子又都超出了作用域,浏览器就会采用更激进的方式调用垃圾回收程序运行
  • 策略1:对象池(类似于线程池的概念):在初始化的某一个时刻,可以创建一个对象池,用来管理一组可回收的对象;应用程序可以想这个对象池请求一个对象、设置其属性,使用它,然后在操作完成后再把他还给对象池,没有发生对象初始化,也就不会有对象更替

4.4 小结

4.4.1 原始值和引用值

  • 原始值大小固定,保存在栈内存上,复制原始值就是copy一个副本
  • 引用值时对象,存储在堆内存上,复制的是引用(指针),而不是对象本身

4.4.2 typeof 和 instanceof

  • typeof 操作符可以确定值的原始类型
  • instanceof用于确定值的引用类型

4.4.3 变量和作用域

  • 任何变量都存在于某个执行上下文中(作用域),这个上下文决定了变量的生命周期
  • 作用域可以分为:全局作用域、函数作用域和块级作用域
  • 代码执行流每进入一个新的作用域,都会创建一个作用域链,用于搜索变量和函数

4.4.4 垃圾回收

  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除
  • 主流的垃圾回收算法是标记清理
  • 引用计数法会带来循环引用的问题,从而造成内存泄露
  • 解除变量的引用不仅可以消除循环引用,对垃圾回收也有帮助,最佳实践~