如有错误烦请指正
文章承接一篇文章用一个例子理解函数的底层处理机制,学习一下浏览器垃圾回收机制
函数执行时,都会经历这样的流程。形成新的上下文,开辟堆内存,进栈执行...如果函数非常多,会一直进栈,开辟堆内存,占内存会越来越大。所以为了优化,浏览器默认有一些回收机制。
GC 浏览器的垃圾回收机制 (内存释放机制)
两种内存释放
栈内存释放
即是否释放在执行环境栈(ECStack)中每次进栈的上下文,例如EC(G),EC(fn)等
- 加载页面,形成一个全局的上下文,只有页面关闭的时候,全局上下文才会释放
- 函数执行,会形成一个私有的上下文进栈执行。大部分情况下,当函数中的代码执行完成,形成的上下文会被出栈释放,以此优化栈内存大小。下面再说不能释放EC(fn)的特殊情况
堆内存释放
即是否释放堆,即Object数据类型开辟的堆内存,包括Object,Array,function等
不同浏览器堆内存回收机制不一样
例如谷歌:查找引用
浏览器在空闲或者执行时间内,查看所有堆内存,把没有被任何东西占用的堆内存释放掉;。但是占用着的堆内存,是不被释放的。
例如ie:引用计数
创建了堆内存,如果被占用一次,则浏览器计数+1,取消占用则计数-1,当记录的数字为0的是时候,则内存释放。某些情况会导致计数混乱,出现内存泄露(该释放的内存没有释放,占用内存)
扩展:js高级程序设计,最后章节有内存泄漏的情况汇总
用例子理解垃圾回收机制
全局上下文是最开始打开页面就形成的,只有在最后关闭页面才会被销毁释放。
函数形成的私有上下文,大部分情况下执行完之后会自动释放掉。
可以看看上一篇文章用一个例子理解函数的底层处理机制的例子中的各个上下文(栈内存)和堆内存是否被释放
- 全局上下文这块栈内存不释放,只有关闭页面才释放。
- 0x000000这块堆内存不释放,因为
x
指向0x000000,还在被占用 - 同样的道理0x000001这块堆内存不释放,因为
fn
指向0x000001
如何释放堆内存?
将x=null;fn=null
或者声明为其他也可,只要将变量指向新的地址,手动取消占用,原来的堆内存就会自动释放,因为没有被任何变量占用
- fn私有上下文这块栈内存在执行完之后就会被释放
- 在函数私有上下文被释放后,内部的私有变量没有了,所指向的堆内存 0x000002也会被释放。
EC(fn)不会释放的情况
例子:
function fn(){
var x = 100
return function() {
console.log('我是return函数')
}
}
var f = fn()
f()
一行行解析
function fn(){...}
执行过程,声明函数的过程。上篇文章已经解释,大体如下:
- 创建一个堆内存假设为0x000
- 声明作用域[[scope]]:EC(G) (在EC(G)全局上下文中创建)
- 将代码字符串存到内存当中
- 声明变量fn
- 将
fn
指向地址0x000
var f = fn()
先把函数执行,把执行完成的返回值赋值给全局f
执行fn()
过程如下
- 形成私有上下文:EC(FN)
- 生成私有变量对象AO(FN)
- 代码执行之前初始化作用域链<EC(FN),EC(G)>即<私有上下文,全局上下文(当前上下文的上级上下文,也是当前函数的作用域)>
- 初始化。
this
,arguments
(先不管) - 形参赋值(无形参,先不管)
- 变量提升(先不管)
- 代码执行
第7步代码执行的过程如下
var x = 100
声明变量,放到私有上下文的私有变量对象当中return
一个function
,即创建一个函数,把函数的堆内存地址返回- 创建函数堆,假设为 0x001
- 声明作用域[[scope]]:EC(FN) (在EC(FN)上下文中创建)
- 将代码字符串存到内存当中
- 把堆内存的地址0x001返回
- 声明全局变量
f
,指向地址0x001
正常情况下,fn
执行完应该会释放,但是现在这种情况不能释放,因为fn
在当前上下文( EC(fn) )下创建了堆0x001,0x001堆的作用域是EC(fn),堆和EC(fn)是有关联的。然后EC(fn)的堆0x001,被全局上下文中的f
变量指向了,如果fn
的内存被释放了,那么0x001也将被释放,f
不再能指向0x001,找不到0x001
总结:函数的当前上下文EC(fn)中开辟的某个"堆内存"(函数或对象),被当前上下文以外的变量(或者其他事物)所占用,此时当前上下文是不能被出栈释放的(不论这个堆内存中的函数或对象有没有用到EC(fn)中的私有变量(在这个例子里就是var x = 100))
接着继续执行f()
一样的过程
- 形成私有上下文:EC(f)
- 生成私有变量对象AO(f)
- 代码执行之前初始化作用域链<EC(f),EC(FN)>即<私有上下文,函数fn的上下文(当前上下文的上级上下文,也是当前函数的作用域)>
- 初始化
this
,arguments
(先不管) - 形参赋值(无形参,先不管)
- 变量提升(先不管)
- 代码执行
最后f
执行完后,EC(f)被释放
闭包
浏览器垃圾回收机制即内存释放机制中有一种释放叫做栈内存释放。函数执行,会形成一个私有的上下文进栈执行。大部分情况下,当函数中的代码执行完成,形成的上下文会被出栈释放,以此优化栈内存大小。 有这样一种特殊情况不能释放函数执行形成的上下文:函数的当前上下文EC(fn)中开辟的某个"堆内存"(函数或对象),被当前上下文以外的变量(或者其他事物)所占用(引用),此时当前上下文是不能被出栈释放的(不论这个堆内存中的被外部引用的这个函数或对象有没有用到当前上下文中的私有变量)。
如上面的例子,函数执行,会形成一个私有的上下文
- 里面的私有变量受到了私有上下文的保护,不受外界干扰
- 有可能形成不被释放的上下文,里面的私有变量和一些值就会被保存起来,这些值可以供"下级"上下文读取使用
我们把函数的这种保存/保护机制称之为闭包
闭包是一种机制
作用域链
函数上下文的作用域链 为<当前函数上下文,当前函数的作用域>
所以fn
的作用域链为<EC(fn),EC(G)>
f
的作用域链为<EC(f),EC(fn)>
寻找当前上下文中的变量就按照这样的顺序,先寻找私有变量,如果没有就按照作用域链一级一级的往上找,比如寻找f中的变量,顺序就是EC(f)=>EC(fn)=>EC(G) 图示如下:
关于闭包作用域练习题
分析两个题目来深入理解闭包的保存/保护机制
题1
let x = 5
function fn(x){
return function(y) {
console.log(y + (++x))
}
}
let f = fn(6)
f(7)
fn(8)(9)
f(10)
console.log(x)
答案:
不画进栈出栈ECStack,直接画上下文标注是否出栈是否进栈
一行一行解析
声明x
和函数fn
let f = fn(6)
执行fn
,传递实参6,把函数返回结果赋值给f
fn
执行,传递参数6- 形成函数私有上下文EC(FN1)
- 生成当前上下文的私有变量对象AO(FN1)
- 生成作用域链<EC(FN1),EC(G)> 即自己所在的上下文和作用域
- 形参赋值,即声明一个形参私有变量,赋值为6
- 代码执行,
return
一个函数,相当于return
一个值,这个值就是这个函数堆的内存地址- 创建函数,堆内存地址为0x001
- 形参为y
- 存储代码字符串
console.log(y + ( ++x))
- 声明作用域[[scope]]: EC(FN1)
- 将0x001返回
- 将0x001赋值给全局下的
f
EC(FN1)上下文不会被释放,因为EC(FN1)上下文中的0x001被全局上下文中的f
占用了,则此上下文不能出栈释放内存(闭包:保存/保护)
执行f(7)
f
执行,传递参数7- 形成函数私有上下文EC(F1)
- 生成当前上下文的私有变量对象AO(F1)
- 生成作用域链<EC(F1),EC(FN1)>
- 形参赋值,声明一个形参私有变量,赋值为7
- 代码执行
console.log(y + ( ++x))
y
:私有的x
:私有中无变量x
,所以**操作上级上下文EC(FN1)**中的x
- 输出14
注意1:闭包,fn1
中的私有变量的值7
会被保存下来
注意2:就算加了括号,也先算5+i
执行fn(8)(9)
,函数每次执行,都会形成全新的私有上下文,和之前执行的上下文没有必然联系,所以输出9+8+1=18
难点和易错点在f(10)
这一行,因为f(7)
已经将fn
中的私有变量x
++过一次了,因为闭包的原因(保存和保护),这个私有变量x
不会销毁,仍然是++过后的值,所以再执行f(10)
进行输出的是x
++两次的值,最终结果是6+1+10+1=18
题2
let a = 0,
b = 0;
function A(a){
A = function (b){
alert(a+b++);
}
alert(a++);
}
A(1);
A(2);
答案:分别alert
1和4