JavaScript高级程序设计学习笔记1——变量、作用域与内存

42 阅读8分钟

1 原始值与引用值

  • 原始值:

值类型
占用空间固定,保存在栈中
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;栈中存储的是基础变量以及一些对象的引用变量,基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。

  • 引用值:
    • 多个值组成的对象
    • 保存在内存中的对象

引用类型
占用空间不固定,保存在堆中
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

  • 操作对象:操作的是对对象的引用,而非直接操作该对象

栈和堆
原始值和引用值的地址都存在栈内,引用值的对象都存在堆内

1.1 动态属性

1 原始值的初始化

  1. 使用原始值字面量形式
  2. 使用new关键字
    • 会创建一个Object类型的实例,可以被添加属性
    • 操作与原始值相似

2 复制值

原始值:直接复制到新变量的位置 引用值:新变量只是复制了指向堆内存的地址

1.2 传递参数

1 函数传参方式

只能按值传递,值会被复制到一个局部变量(即arguments对象的一个槽位)。

如果是按引用传递参数会导致对函数内部变量的修改反映到函数外部

举个例子

function setName(obj) { // 此时obj为形参
  obj.name = "Nicholas"; // 1
  obj = new Object(); // 2
  obj.name = "Greg"; // 3
}  
let person = new Object(); 
setName(person); // 此时person为实参
console.log(person.name);  // "Nicholas"

函数的形参会复制实参的值(原始值/值类型)或实参的地址(引用值),所以1处是直接改了实参的对象,2处将形参在栈内指向了一个新的对象,3处直接修改了这个新对象的值,无法影响到实参的对象,所以实参就是1修改完的对象,因此打印出来的是Nicholas

1.3 确定类型

1 如何确定对象的类型(值的引用类型)

instanceof 操作符

  • 语法:result = variable instanceof constructor
  • 引用类型由其原型链决定
  • 对原始值使用都会返回false

2 返回function的情况

  • 一般来说检测函数会返回“function”
  • 特殊情况:在旧版本中,Safari和Chrome使用typeof操作符检测正则表达式时会返回“function”
    • ECMA-262规定,任何实现内部[[Call]]方法的对象都应该在typeof检测时返回“function”

2 执行上下文与作用域

  • 上下文:决定变量or函数可以访问的数据和它们的行为

上下文和作用域的区别

定义作用创建时机生命周期
上下文代码执行时的环境或状态代码执行时会创建一个执行上下文,包含函数执行所需的所有信息,如变量对象、作用域链和this关键字在代码执行时创建的每调用一个函数,会创建一个新的执行上下文,并在代码执行完成后销毁该上下文
作用域变量&函数可访问范围决定变量or函数在何处可以被访问or引用代码编写时确定,定义了变量的可见性和生命周期全局作用域:全局代码执行前确定;函数作用域:函数定义时确定
  • 变量对象(vo,variable object):每个上下文都有一个关联的变量对象,在这个上下文中定义的所有变量和函数都存在这个对象中 ^3kh5b0
  • 全局上下文:最外层的上下文
    • 浏览器里:window对象
      • 通过var定义的全局变量和函数都会成为window对象的属性和方法
  • 执行流程
    • 执行函数调用时,函数上下文被推入执行上下文栈
    • 函数执行完后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文
  • 销毁
    • 上下文在其所有代码执行完毕后会被销毁
      • 一旦用完了就销毁,包括变量和函数
    • 全局上下文在应用程序退出前才会被销毁
  • 作用域链
    • 概念:在代码执行时出现的,用于体现变量对象和函数作用顺序的链
    • 组成
      • 最前端:
        • 正在执行的上下文的变量对象
        • 函数:活动对象(activation object)用作变量对象
          • 活动对象最初只有arguments
      • 后面的变量对象来自含有上一个变量对象的上下文
      • 最末端:全局上下文的变量对象

一点思考
联系函数传参方式,活动对象应该是指在函数中的活跃变量;作用域链组成与作用域嵌套相联系,可以想象作用域链的变量对象排列顺序

2.1 作用域链增强

  • 执行上下文的分类
    • 全局上下文
    • 函数上下文
    • 块作用域上下文
  • 增强作用域链
    • 概念:导致在作用域链前端临时添加一个上下文,代码执行后删除
    • 方法
      • try/catch语句的catch
        • 会创建一个新的变量对象,这个对象会包含要抛出的错误对象的声明
      • with语句
        • 会向作用域链前端添加指定的对象
        • 即语句所在函数的作用域内

2.2 变量声明

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

  • 使用var声明变量时,变量会被自动添加到最接近的上下文
  • 未经声明就初始化,会被添加到全局上下文

2 使用let的块级作用域声明

  • 块级作用域:由最近的一对{}界定
    • 不仅是函数,由{}组成的块也是
  • SyntaxError:用letconst在同一作用域内声明两次时报错
    • 重复的var声明会被忽略

3 使用const的常量声明

  • const声明的应用范围:顶级原语或对象
    • 见const声明
  • Object.freeze():使得const声明的对象无法修改
    • 尝试修改对象时不会报错,但是会静默失败

4标识符查找

  • 流程
    • 从作用域链前端开始查找,找到就停止,没找到就沿着作用域链继续查找
    • 作用域链中的对象也有一个原型链,因此搜索时也会沿着原型链查找

3 垃圾回收

3.1 标记清理

  • 概念:变量在某些特殊情况下被打上对应的标记,JavaScript的垃圾回收程序根据自己的规则清理掉某些标记的变量
  • JavaScript垃圾清理的方法:
    • 垃圾回收程序先标记内存里的所有变量,再将所有在上下文中的变量、被上下文中的变量引用的变量的标记去掉(正在使用中的变量),剩下的就都是要清掉的变量了
    • 之后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回内存

3.2 引用计数

  • 只要引用一次就打上标记,不再引用就去除标记,当引用数为0时,垃圾回收程序运行时会释放这些值的内存
  • 不常用,因为会出现相互引用导致内存永远无法释放

3.3 内存管理

  • 解除引用:如果数据不再必要,那么将其主动设置为null,就可以释放其引用
    • 并不会立即被回收,只是为了确保相关值不在上下文里了,使得下次垃圾回收时被回收

1 通过constlet声明提升性能

它们的作用域是块作用域,因此出了块作用域就可以及时被释放

2 隐藏类和删除操作

  • 创建时,尽量避免“先创建再补充”式的动态属性赋值,尽量在构造函数中一次性声明所有的属性,用或不用通过传参的方式赋值
  • 删除时,尽量把不想要的属性设置为null,而不是删除属性

3 内存泄漏

可能情况

  • 意外声明全局变量
  • 定时器的回调通过闭包引用了外部变量
  • 使用JavaScript闭包

4 静态分配与对象池

  • 浏览器决定何时运行垃圾回收程序:对象更替的速度(对象初始化的速度)
    • 初始化的越多,浏览器的回收程序就触发的越频繁
  • 减少回收程序触发频率的方法:
    • 不要动态创建矢量对象,通过传参的方式修改对象
      • 使用对象池,在初始化的某一时刻创建对象池,管理一组可回收的对象
    • 减少动态分配操作,尽量静态设置一定量的内存,初始化时设置一个足够用的数组