写在前面
往期内容
函数的声明与执行
声明一个函数
当我们声明了一个函数之后,发生了什么操作呢?
- 首先开辟一个堆内存,用来存放要声明的函数
- 声明函数的作用域
[[scope]]:EC(xxx)- 函数是在哪个作用域中创建的,那么函数的作用域就是谁
- 函数真正的作用域是创建函数时所在的作用域
- 将函数中的
形参进行声明 - 将函数体内的代码当成字符串,存储在函数的堆内存中
- 当创建函数时,函数执行体中的内容会被存储为一堆字符串
- 所以函数只要不执行,函数体中的内容就是一堆字符串,没有实际意义
- 函数形成之后,把函数的堆内存地址放入栈中供之后调用
注:其实 function fn(){}和 var fn = function(){} ,这两个发生的操作是一样的,区别只是 var 存在变量提升
执行一个函数
函数在执行时,又做了哪些操作呢?
- 形成一个全新的私有上下文
EC(FN),供函数体中的代码执行 - 上下文形成后,在该私有上下文中生成一个存放私有变量的变量对象 AO(FN)
- 在函数中代码执行之前要进行:
- 初始化函数作用域链
scope-chain,链的左侧<EC(FN)(自己的上下文),[[scope]](函数的作用域)> - 初始化
THIS(箭头函数中没有THIS) - 初始化
ARGUMENTS实参集合(箭头函数中没有) - 形参赋值:形参变量是函数的私有变量,需要存储在
AO中。函数中定义的值也是私有变量 - 变量提升(在私有上下文中声明的变量都是私有变量)
- 初始化函数作用域链
- 函数进执行栈执行
- 代码执行
- 将之前在函数堆内存中存储的字符串,在当前私有上下文中执行
- 作用域链查找机制:在代码执行中,遇到一个变量,我们先看一下是否为自己的私有变量,如果是自己的私有变量,接下来所有操作都是私有的(和外界没有直接的联系);如果不是自己私有的,则按照
scope-chain,向上级上下文中查找(如果此变量是上级的私有变量,接下来的操作都是操作上级上下文中的变量)。。。 一直找到 EC(G)为止
- 根据实际情况确定当前上下文是否出栈释放
- 一般情况下,为了保证栈内存的大小(内存优化),如果当前函数执行产生的上下文,在进栈且代码执行完成之后,会把此上下文移出栈(上下文释放掉了,之前在上下文中存储的私有变量等信息也就跟着释放了)
- 全局上下文是在打开页面时生成的,也需要在关闭页面的时候释放掉(只有页面关闭才会被释放掉)
- 特殊情况下,如果当前上下文中的某些内容,被当前上下文以外的东西占用,那么当前上下文就不会被释放的(上下文中存储的变量等信息也被保留下来了)。此时就形成了大家通俗理解上的闭包
- 还有一种情况
fn(a)(b),这种情况下属于临时占用,上下文临时不释放
- 如果函数在执行完一次后二次执行,会形成一个全新的私有上下文,把之前做过的事情原封不动的再执行一次
闭包
很多同学认为只要函数中return一个函数,就形成了一个闭包,这样理解是不准确的。
当然,以下描述我也是赞同的,但是我总感觉没有把原因给解释出来
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数继续访问定义时的词法作用域
我个人更加赞同的理解是:
首先明确,闭包是函数运行时产生的一种机制。
闭包的作用:保护和保存
当函数执行会时,会形成一个全新的私有上下文,这样可以保护该上下文中的私有变量和外界互不干扰(保护机制)。如果当前上下文中的某些内容被当前上下文以外的内容占用,则当前上下文不能被释放,其中的私有变量和值也不会被释放(保存机制)。这种保护和保存的机制称为闭包。
以上描述可能同学们觉得不够具体,那我们就拿一段经典代码来分析一下
var a= 100;
var b= 200;
function foo(){
var a = 0;
var b = 1;
function closure(){
a++;
b++;
return a+b
}
return closure
}
var x = foo();
此段代码的运行过程为:(忽略执行环境栈ECStack、GO、变量提升等内容)
- 全局代码运行,形成全局上下文
EC(G), 形成全局变量对象VO(G),VO(G)中生命并定义变量a,b - 函数声明并定义(可以参照上文,<声明一个函数>),在全局变量对象
VO(G)中生成变量foo跟函数堆内存地址的对应关系 - 函数执行,形成一个私有上下文
EC(FOO)(闭包作用域),并进行初始化操作(可以参照上文,<执行一个函数>) - 在此作用域下,又声明并定义了一个函数
closure,(会生成一个函数closure的堆内存, 这个函数中的[[scope]]: EC(FOO)),并在私有上下文EC(FOO)的私有变量对象中生成变量closure和函数堆内存地址的对应关系。最后返回closure函数(相当于函数堆内存地址的返回) var x = foo(),相当于在全局变量对象中声明并定义了一个变量 x,x 和 closure 函数的堆内存地址相关联- 至此,闭包的保护和保存两种机制都得到了实现:
- 保护: 在
foo函数执行时,形成了一个新的私有上下文,使得其中定义的变量a,b与全局上下文EC(G)中的变量a,b互相之间不干扰 - 保存:由于全局上下文中的变量
x和函数执行上下文EC(FOO)中定义的函数closure有关联关系,所以函数执行上下文EC(FOO)不能被内存回收机制所释放。所以其中的变量和值就被保存了下来
- 保护: 在
- 我们来验证一下,如下图
闭包的应用
- 实战用途,简单举例:for 循环函数中 i 值问题
- 高阶编程:柯里化/惰性函数/compose 函数
- 源码分析:JQ/LODASH/REACT(REDUX/高阶组件/Hooks)
- 自己封装插件组件的时候
- 。。。
内存优化和浏览器的垃圾回收机制
首先我们来了解一下 JS 中的内存优化策略。
JS 中的内存优化
- 一般情况下,函数执行完,所形成的的上下文会被出栈释放掉
- 特殊情况下:当前上下文中某些内容被上下文以外的事务占用了,此时不能出栈释放
- 全局上下文:加载页面时创建的,页面关闭的时候才会被释放掉
浏览器垃圾回收机制
那么浏览器是如何做垃圾回收,并实现内存优化的呢?
强推高程 3 上关于垃圾回收机制的内容,对垃圾回收机制讲解的非常棒(P.78)。我们基于这部分内容进行总结
引用计数:
不太常见,只在低版本 IE 浏览器和一些远古浏览器中使用。
含义:
- 引用计数的含义是跟踪记录每个值被引用的次数。
- 当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是
1。 - 如果同一个值又被赋给另一个变量,则该值的引用次数
+1 - 相反,如果包含对这个值引用的变量又取得了另一个值,则原先的值的引用次数
-1 - 当某个值的引用次数变成
0时,就会被垃圾收集器下次运行时给销毁,释放引用次数为0的值的内存
严重问题:循环引用
function problem(){
var objA = new Object();
var objB = new Object();
objA.a = objB;
objB.b = objA;
}
以上例子中,objA和objB通过各自的属性相互引用,他们的引用次数都是 2。由于相互引用,它们的引用次数永远不会为 0,如果这个函数多次运行或者上例情况经常发生,就会造成内存泄漏。
标记清除:
JS 中最常用的垃圾收集方法,目前主流浏览器都采用此种方法。
- 当变量进入环境时,通过某种方法(比方说翻转某个特殊的记录位)来标记一个变量进入环境
- 当变量离开环境时,则将其标记为离开环境
- 垃圾收集器在运行的时候,会给所有在内存中的变量都加上标记。然后再去除环境中的变量和所有被环境中变量所引用的变量的标记
- 其他仍有标记的变量就会被认为是准备要删除掉的变量,因为它们已经无法被访问到了
- 最后,垃圾收集器完成内存清除工作
总结一下
引用计数: 在某些情况下会导致计数混乱,这样会造成内存不能被释放(内存泄漏)
检测引用(标记清除):浏览器在空闲时候会依次检测所有的堆内存,把没有被任何事务占用的堆内存释放掉,借此优化内存
手动释放内存,其实就是解除占用(解除指针指向),一般是手动赋值为 null(空指针对象)
。。。
写在最后
欢迎访问我的博客fxflying.com