JavaScript 高级程序设计(第 4 版)- 变量、作用域和内存

2023-05-17 14:57:09 浏览数 (2)

# 原始值与引用值

  • 在把一个值赋给变量时,JS引擎必须确定这个值是原始值还是引用值(原始值有6种:Undefined,Null,Boolean,Number,String和Symbol)
  • 原始值(primitive value)就是最简单的数据(原始值大小固定,因此保存在栈内存)
    • 保存原始值的变量是按值(by value)访问的,因为操作的就是存储在变量中的实际值
  • 引用值(reference value)则是由多个值构成的对象(引用值是对象,保存在堆内存)
    • 引用值是保存在内存中的对象
    • 与其他语言不同,JS不允许直接访问内存位置,即不能直接操作对象所在的内存空间
    • 在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身
    • 保存引用值的变量是按引用(by reference)访问的

# 动态属性

  • 引用值可以随时添加、修改和删除其属性和方法
  • 原始值不能有属性
  • 只有引用值可以动态添加后面可以使用的属性
  • 原始类型的初始化可以只使用原始字面量形式。如果使用的是new关键字,则JS会创建一个Object类型的实例,但其行为类似原始值

# 复制值

  • 在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。复制后的两个变量独立使用,互不干扰
  • 把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。但本质是赋值一个指针,指向存储在堆内存中的对象。复制后的连个变量实际上指向同一个对象

# 传递参数

  • ECMAScript 中所有函数的参数都是按值传递的。即函数外的值会被复制到函数内部的参数中,就像一个变量复制到另一个变量一样。
  • 如果是原始值,那么就跟原始值变量的赋值一样,如果是引用值,就跟引用值变量的复制一样
    • 在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,就是arguments对象中的一个槽位)
    • 在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部(这在ECMAScript中是不可能的)
代码语言:javascript复制
function addTen(num) {
  num  = 10;
  return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20
console.log(result); // 30

function setName(obj) {
  obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

function setName2(obj) { // 此处是按值传入
  obj.name = "Nicholas";
  obj = new Object(); // 所以修改obj的指向会影响外部person2的指向
  obj.name = "Greg";
}
let person2 = new Object();
setName2(person2);
console.log(person2.name); // "Nicholas"

# 确定类型

  • 判断一个变量是否是字符串、数值、布尔值或undefined最好的方式是typeof
  • 如果值是对象或null,那么typeof返回"object"
  • 如果变量是给定引用类型的实例,则instanceof操作符返回true
  • 所有引用值都是 Object 的实例,因此通过instanceof 操作符检测任何引用值和Object构造函数都会返回true
  • 如果用instanceof检测原始值,这会返回false

# 执行上下文与作用域

  • 每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上
  • 全局上下文是最外层的上下文。
    • 浏览器中,全局上下文即window对象,所有通过var定义的全局变量和函数都会成为window对象的属性和方法
    • 使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
    • 上下文再起所有代码都被执行完毕后会被销毁
  • 每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链。该作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端
    • 如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments
    • 作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文,类推直至全局上下文
    • 全局上下文的变量对象始终是作用域链的最后一个变量对象
  • 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西
  • 上下文之间的连接是线性的、有序的。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索

# 作用域链增强

执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。 以下两种情况会在作用域链前端添加一个变量对象。

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

# 变量声明

  • 使用var的函数作用域声明
    • 变量会被自动添加到最近的上下文
      • 在函数中,最接近的上下文就是函数的局部上下文
      • 在with语句中,最接近的上下文也是函数上下文
      • 如果变量未经声明就被初始化了,那么它会被自动添加到全局上下文
    • var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前(即变量提升)
  • 使用let的块级作用域声明
    • 和var类似,但是其作用域是块级的(块级作用域由最近的一对花括号界定)
    • let在同一作用域内不能声明两次,重复的var声明会被忽略,而重复的let声明会抛出SyntaxError
    • let的行为非常适合在循环中声明迭代变量,使用var声明的迭代变量会泄露到循环外部
    • 严格讲,let在JS运行时中也会被提升,但由于“暂时性死区”的缘故,实际上不能在声明之前使用let变量
  • 使用const的常量声明
    • 使用const声明的变量必须同时初始化为某个值
    • 一经声明,在其生命周期的任何时候都不能再被重新赋予新值
    • 赋值为对象的const变量不能再被重新赋值为其他引用值,当对象的键则不受限制
    • 如果想让整个对象都不能修改,可以使用Object.freeze()
  • 标识符查找
    • 当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么
    • 搜索开始于作用域链前端,以给定的名称搜索对应的标识符(沿着作用域链向上搜索(搜索会涉及每个对象的原型链))

# 垃圾回收

JS是使用垃圾回收的语言,即执行环境负责在代码执行时管理内存。基本思路:确定那个变量不会再使用,然后释放它占用的内存。回收过程是周期性自动运行的。主要标记策略:标记清理和引用计数。

  • 标记清理
    • 当变量进入上下文,这个变量会被加上存在于上下文中的标记,当变量离开上下文时,也会被加上离开上下文的标记。
    • 垃圾回收程序运行的时,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回他们的内存。
  • 引用计数
    • 对每个值都记录他被引用的次数。声明变量并给它赋一个值时,这个值的引用数为1.如果同一个值又被赋给另一个变量,那么引用数加1.如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。引用值为0时,就说明没办法再访问这个值了,就可以安全收回其内存了。
  • 性能
    • 垃圾回收的时间调度很重要,故最好写代码时做到:无论什么时候开始收集垃圾,都能让他尽快结束工作

# 内存管理

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,就设置为null,从而释放引用(解除引用)。

通过const和let声明提升性能

  • const和let都以块作为作用域,所以相对于var,使用这两个新关键字可能会更早让垃圾回收程序介入,尽早回收内存

隐藏类和删除操作

  • 通过共享一个隐藏类,从而带来潜在的性能提升
    • 使用delete关键字会导致生成相同的隐藏类片段,动态删除属性和动态添加属性导致的后果一样
    • 最佳实践是把不需要的属性设置为null,这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收的效果
代码语言:javascript复制
function Article() {
  this.title = 'Inauguration Ceremony Features Kazoo Band';
  this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null;

内存泄漏

  • 在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个大问题
  • JS中的内存泄漏大部分是由不合理的引用导致的
  • 意外声明全局变量是最常见但也是最容易修复的内存泄漏问题
  • 定时器也可能会导致内存泄漏
  • 使用JS闭包很容易在不知不觉中造成内存泄漏

静态分配与对象池

开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。

0 人点赞