一文搞懂JavaScript变量、作用域和内存

ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值
原生值和引用值的区别 什么是原始值:
最简单的值 例如:Undefined、Null、Boolean、Number、String 、Symbol,BigInt。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值
什么是引用值:
由多个值构成的对象 例如:Object。引用值是保存在内存中的对象,保存引用值的变量是按引用(by reference)访问的,由于JavaScript 不允许直接访问内存位置,实际上操作的是对该对象的引用(reference)而非实际的对象本身。
区别:
  • 动态属性:
    原始值:不能有属性
    引用值:可以随时添加、修改和删除其属性和方法
  • 复制值:
    原始值:复制值变量可以独立使用,互不干扰。
    let num=123; let num1=num; console.log(num,num1)//123,123 num=100; console.log(num,num1)//100,123

    引用值:复制值,复制的是栈中的指针(堆中对象的地址),指向的是同一个对象
    let obj={name:"lly",sex:"Man"} let obj1=obj; console.log(obj,obj1)//{name:"lly",sex:"Man"},{name:"lly",sex:"Man"} obj.age=20 console.log(obj,obj1)//{name:"lly",sex:"Man",age:20},{name:"lly",sex:"Man",age:20}

  • 传递参数:
    原始值:跟原始值变量的复制一样
    function add(num) { num += 10; return num; } let count = 20; let result = add(count); console.log(count); // 20,没有变化 console.log(result); // 30

    引用值:跟引用值变量的复制一样
    function setName(obj) { obj.name = "lly"; } let person = new Object(); setName(person); console.log(person.name); // "lly"

  • 关于类型的判断的方法:
    原始类型值:typeof
    引用类型值:instanceof
    比较全面的判断方法:Object.prototype.toString.call(要判断的类型值);
    执行上下文与作用域什么是执行上下文:
    当前 JavaScript 代码被解析和执行时所在环境的抽象概念
    执行上下文的种类
    1.全局执行上下文
    3.函数执行上下文
    2.块级执行上下文
    什么是作用域:
    一个不让变量外泄暴露的独立区域,简单说就是隔离变量的空间,特质:同名变量在不同作用域下互不冲突。
    作用域的种类:
  • 全局作域域
  • 函数作用域
  • 块级作用域
变量声明的种类以及特征
  1. var 函数作用域声明
    存在变量提升,在同一作用域内可以重复声明同一变量
  2. let 块级作用域声明
    也存在变量提升只是暂时性死区缘故导致变量声明之前不可用、在同一作用域内不能声明两次
  3. const 常量声明
    声明变量必须同时初始化值,一经声明不能重新赋值(对于初始值是原始值,不可重新赋值,对于初始值是引用值,不能重新赋值其他引用,对引用值得键值没有限制),其他特质和let相同。
    垃圾回收机制JavaScript是使用垃圾回收的语言。执行环境负责在代码执行时管理内存。通过自动内存管理实现内存分配和闲置资源回收。实现思路:确定那个变量不在使用,就释放所占内存。垃圾回收的标记策略主要采用:标记清理和引用计数。
    标记清理的原理
    离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除,先给当前不使用的值加上标记,再回来回收它们的内存
    引用计数的原理(不常用的)
    对每个值进行引用次数记录,当引用次数为0时,垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
    引用次数的规则:
    声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1
    性能
    垃圾回收程序会周期性运行,频繁的调度垃圾回收程序会严重影响性能,所以浏览器采用了许多性能优化的方案:
    1.分代收集:
    对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少
2.增量收集:
如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。
3.闲时收集:
垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
内存管理
1.通过 const 和 let 声明提升性能:
由于const 和 let 以块为作用域不是函数作用域,更早地让垃圾回
收程序介入,尽早回收应该回收的内存。从而改进垃圾回收的过程。
2.隐藏类和删除操作:
隐藏类:
JavaScript引擎为了提高性能引入了隐藏类,运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。
function people() { this.name= 'lly'; } let a1 = new people(); let a2 = new people(); a2.sex="Man"

解决方案就是避免 JavaScript 的“先创建再补充”式的动态属性赋值,并在构造函数中一次性声明所有属性。
function people(sex) { this.name= 'lly'; this.sex=sex } let a1 = new people(); let a2 = new people("Man");

删除操作:
使用 delete 关键字会导致生成相同的隐藏类片段,最佳实践是把不想要的属性设置为 null。
function people() { this.name= 'lly'; this.sex="Man" } let a1 = new people(); let a2 = new people(); delete a1.sex; //不推荐 a1.sex=null//最佳实践

3.导致内存泄露原因以及解决方法:
  • 闭包:手动设置null,切断引用
  • 意外的全局变量:使用严格模式或者前面加上声明关键字
  • 定时器:不在需要定时器时清除定时器
  • DOM/BOM 对象泄漏window.onunload事件上释放内存
【一文搞懂JavaScript变量、作用域和内存】4.静态分配与对象池:
对象池:
JavaScript优化的一种策略,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行
静态分配:
优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑。

    推荐阅读