3. 作用域链和闭包
1 基础
1. 内存管理
1.1 生命周期
- 申请:程序申请内存,系统分配你需要的内存;
- 使用:程序使用分配好的内存,如存放对象;
- 释放:当程序使用完毕后,系统释放内存资源。
- 手动管理内存:如 C、C++,包括早期的 OC,需要手动来管理内存的申请和释放(malloc 和 free 函数);
- 自动管理内存:比如 Java、JavaScript、Python、Swift、Dart 等,它们有自动帮助我们管理内存;
1.2 Js 的内存管理
- 基本数据类型:内存的分配会在执行时, 直接在 调用栈 进行分配;
- 复杂数据类型:内存的分配会在 堆内存 中开辟一块空间,然后在调用栈中,有一个指针指向堆内存的变量。
闭包也保存在堆内存中。
1.3 垃圾回收
垃圾回收分为:栈的垃圾回收,堆的垃圾回收。
- 栈的垃圾回收依赖于调用栈的出栈;
- 堆的垃圾回收依赖于 GC 机制。
Garbage Collection,GC。是现代编程语言的垃圾回收机制。
常见有三种 GC 算法(浏览器相关文章 存储机制 中有介绍):
- 引用计数
- 当一个对象有一个引用指向它时,那么这个对象的引用就 +1,当一个对象的引用为 0 时,这个对象就可以被销毁掉;
- 这个算法有一个很大的弊端就是会产生循环引用;
- 标记清除
- 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象;
- 这个算法可以很好的解决循环引用的问题,Js 使用的是该方法。
1.4 Js 中垃圾回收
更详细内容,见:浏览器-存储机制
基本原理:
- 代际假说:大部分对象在内存中存在的时间很短。不死的对象,会活得更久。
- 分代收集:在 V8 中会 把堆分为 新生代(副垃圾回收器) 和 老生代(主垃圾回收器) 两个区域。
工作流程:
-
主回收器工作流程 (3):标记对象、回收内存、整理内存。
-
副回收器的工作流程(4):标记对象、回收内存、整理内存、角色反转、对象晋升。
-
Scavenge 算法:新生代把空间对半划分:对象区域 + 空闲区域。
-
标记对象。标记空间中活动对象和非活动的垃圾对象。
-
回收内存 + 整理内存。把存活的活动对象复制到空闲区域中,在复制的过程中,同时把这些对象进行有序排列。所以回收内存的同时也完成了碎片的整理。
-
角色反转。原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。
-
对象晋升。经过 两次 垃圾回收依然还存活的对象,会被移动到老生区中。
-
-
对象晋升:
新生代中用 **标记 - 清除(Mark-Sweep)**和 **标记 - 整理(Mark-Compact)**来处理。流程如下:
- 标记对象。挨个遍历调用栈中的全部变量,以每一个变量为根元素,遍历这组根元素。所有遍历过程中,能到达的元素称为 活动对象,没有到达的元素判断为 垃圾数据。
- 回收内存:标记 - 清除(Mark-Sweep)。直接清理掉标记为垃圾数据的对象。
- 整理内存:标记 - 整理(Mark-Compact)。并不是每次都执行整理工作。执行时,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优化:
增量标记(Incremental Marking),解决全停顿(Stop-The-World)现象。
- 全停顿:这是因为 Js 运行在浏览器的主线程之上,而一旦执行垃圾回收算法,就会阻塞接下来的 js 脚本执行,造成卡顿。
- 增量标记:V8 把标记过程划分为多个子过程,让 GC 回收不会一次性完成,而是分多次和 Js 脚本交替进行,减小 Js 阻塞,减小页面动画等卡顿(16sm 一帧,这里涉及到浏览器的调度机制)。
2. 作用域链
首先,通过一段代码理解作用域链:
当一个代码块嵌套在另一个代码块中时,就发生了作用域的嵌套;作用域之前发生嵌套关系,就形成了作用域链。
作用域链是 静态 的,它只受变量和函数的声明位置决定。不同作用域的嵌套关系在分析代码时可以得出,在 编译前 引擎便能确定,和 执行上下文无关 。
执行上下文/作用域链
当引擎在逐行执行代码时,如果遇到一个函数调用,引擎就会对应的创建一个执行上下文。每个执行上下文中都有一个 outer
(在变量环境中)。这个 outer
指向该函数定义的时候所在作用域,有时它是一个执行上下文,有时它是一个闭包。
- 更多关于闭包的知识,参考 "