《JavaScript高级程序设计》第四章 变量作用域及内存 学习笔记

224 阅读6分钟

1、原始值和引用值

  • 原始值:最简单的数据
    • 按值访问,操作的是存储在变量的实际值
  • 引用值:由多个值构成的对象
    • 不允许直接访问内存位置,实际操作的是对该对象的引用而非实际对象的本身
    • 保存引用值的变量是按引用访问的。

1、动态属性

  • 引用值可以随时增删改属性和方法
  • 原始值不能有属性,虽然不会报错,但无效
  • 原始类型初始化可以值使用原始字面量形式,如果用new会创建Object类型的实例,但行为类似原始值

2、复制值

  • 原始值会被复制到新的位置
  • 引用值也会把存储的值复制到新变量的位置,但这里复制的是一个指针,实际指向堆中的同一个对象。

3、传递参数

  • 函数的参数都是按值传递的,即函数外地值会被复制到函数内部的参数中

  • 按值传递参数时,值会被复制到局部变量(arguments的一个槽位)。

  • 如果是按引用传递参数,只在内存中的地址会被保存在一个局部变量,意味着对本地变量的修改会反应到函数外部(不可能)

  • 原始值显而易见,引用值也是按值传递而不是按引用传递

    function setName(obj) {
      obj.name = 'firstSet'
      obj = new Object()
      obj.name = 'secondSet'
    }
    let person = new Object()
    setName(person)
    persion.name // 'firstSet'(而不是secondSet)
    
  • 函数的参数就是局部变量

4、确定类型

  • typeof 对原始值很有作用,但对引用值作用不大,返回object
  • instanceof 用来知道是什么类型的对象
  • instanceof 检测所有引用值和Object构造函数都会返回true
  • instanceof 检测所有原始值都会返回false
  • typeof 能检测函数

2、执行上下文与作用域

  • 上下文决定了可以访问哪些数据,以及它们的行为,每个上下文都有一个关联的变量对象
  • 全局上下文是最外层的上下文,浏览器 window对象
  • 上下文在其所有代码都执行完毕后会笑会,包括定义在上面的所有变量和函数
  • 每个函数都有自己的上下文,执行流进入函数,函数上下文被推到一个上下文栈上。函数执行完毕,上下文栈会弹出函数上下文,将控制权返还给之前的上下文。
  • 作用域链,决定各级上下文中的代码在访问变量和函数时的顺序
  • 局部作用域定义的变量可用于在局部上下文中替换全局变量。
  • 每个上下文都能到上一级上下文中去搜索变量和函数,但任何上下文都不能去下一级上下文中去搜索。

1、作用域链增强

  • 增强上下文,导致作用域链前端临时添加一个上下文,执行后会删除
    • try/catch语句的catch 创建新的变量对象,包含要抛出的错误
    • with 前端添加指定对象

2、变量声明

1、var 函数作用域

  • 变量会被自动添加到最接近的上下文,函数中就是函数的局部上下文/
  • 如果未经声明就初始化了,那么就会自动添加到全局上下文。
  • 提升:var声明会拿到全局或者函数的作用域顶部,位于所有代码之前

2、let 块作用域

  • 作用域:块级(最近的一对包含花括号{}界定)
  • 同一个作用域中不能声明两次
  • 适合循环中声明迭代变量
  • “暂时性死区”,不能在声明前使用

3、const 常量

  • 声明的同时必须初始化
  • 不能重新赋值
  • 其他特征等同let
  • 只应用到顶级原语或对象(属性可修改),如果想让整个对象都不可修改,可以用Object.freeze()
  • 尽量能用const就用const

4、标志符查找

3、垃圾回收

  • 自动内存管理实现内存分配和闲置资源回收。
  • 周期性释放不再会使用的变量,释放它占用的内存。
  • 函数中,局部变量在执行时存在,此时栈或堆内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不需要那个局部变量了,它占用的内存就可以释放。

1、标记清理

  • 最常用的垃圾回收策略
  • 标记方法
    • 垃圾回收机制运行时。标记内存中存储的所有变量。
    • 将所有在上下文中的变量,以及被上下文中变量引用的变量的标记去掉
    • 在此之后再被加上标记的变量就是待删除的了。
    • 随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

2、引用计数

  • 对每个值都记录它被引用的次数。
  • 思路
    • 声明并赋引用值时,引用数 1
    • 同一个值赋给另一个值 引用数 加1
    • 被其他值覆盖 引用数 减 1
    • 当引用数为0,可以被回收。
  • 问题:循环引用

3、性能

  • 无论什么时候开始收集垃圾,都尽快让他结束工作

4、内存管理

  • 将内存占用量保持较小的值,可以使页面性能更好,不需要的数据设置为null,也叫解除引用。适合全局变量,全局属性。

1、通过const和let声明提升性能

  • 块作用域,可以更早让垃圾回收程序介入,尽快回收内存。
  • 块作用域比函数作用域更早终止的时候,就可能会发生。

2、隐藏类和删除操作

  • 避免先创建再补充,在构造函数中一次性声明所有属性,共享一个隐藏类

    function Art() {
      this.title = 'title'
    }
    let a1 = new Art()
    let a2 = new Art()
    a2.author = 'author' // bad
    
    function Art2(author) {
      this.title = 'title'
      this.author = author
    }
    let a3 = new Art1()
    let a4 = new Art1('author') // good
    
  • 动态删除属性(delete)和动态添加属性效果一样,可以改为设置为null。

3、内存泄露

  • 原因:不合理的引用
  • 方式
    • 意外的声明全局变量
    • 定时器,通过闭包引用外部变量
    • 闭包

4、静态分配与对象池

  • 间接的控制触发垃圾回收的条件,合理使用分配的内存,避免多余垃圾回收

  • 避免动态创建新对象,修改后返回。

    function addVetor(a, b) {
    	let resultant = new Vetor()
      resultant.x = a.x + b.x
      resultant.y = a.y + b.y
      return resultant
    } // bad
    
    function addVetor(a, b, resultant) {
    	resultant.x = a.x + b.x
      resultant.y = a.y + b.y
      return resultant
    } // good
    
    • 其他地方创建resultant,使用对象池。
  • 对象池

    • 初始化的某一时刻,创建一个对象池,用来管理一组可回收的对象。
    • 对象不存在时创建新的,存在时复用存在的。
    • 极端形式,不多见,一般不考虑。