作用域链 [[Scopes]]
- 每一个函数都有一个 [[Scopes]] 属性,其存储的是这个函数运行时的作用域链,除了当前函数的词法环境LE,作用域链的其他部分都会在其父函数预编译时添加到函数的 [[Scopes]] 属性上(因为父函数也需要预编译后才能确定自己的 函数词法环境(function environment)),所以 js 的作用域是词法作用域。
// 1: 全局词法环境global.LE = {t,fun}
let t = 111
function fun(){
// 3: fun.LE = {a,b,fun1}
let a = 1
let b = 2
function fun1() {
// 5: fun1.LE = {c}
let c = 3
}
// 4: fun1.[[Scopes]] = [global.LE, fun.LE]
}
// 2: fun.[[Scopes]] = [global.LE]
fun()
- 上面代码在 fun() 被调用前,会立即预编译 fun 函数,这一步会得到 fun 的词法环境(LE),然后运行 fun 函数,在执行到 let a = 1 的时候,会将变量对象到 a 属性改成 1。后面也是一样
闭包对象 Closure
- [[Scopes]] + Closure 就是闭包的方案
- Closure 跟 [[Scopes]] 一样会在函数预编译时被确定,区别是当前函数的 [[Scopes]] 是在其父函数预编译时确定, 而 Closure 是在当前函数预编译时确定(在当前函数执行上下文创建完成入栈后就开始创建闭包对象了)。
- 在为一个函数绑定词法作用域时,并不会粗暴的直接把父函数的 LE 放入其 [[Scopes]] 中,而是会分析这个函数中会使用父函数的 LE 中的哪些变量,而这些可能会被使用到的变量会被存储在一个叫做 Closure 的对象中,每一个函数都有且只有一个 Closure 对象,最终这个 Closure 将会代替父函数的 LE 出现在子函数的 [[Scopes]] 中
- 每一次遇到内部声明的函数/方法时都会这么做,无论其内部函数/方法的声明嵌套有多深,并且他们使用的都是同一个 Closure 对象。并且这个过程 是在预编译时进行的而不是在函数运行时。
// 1: global.LE = {t,fun}
var t = 111
// 2: fun.[[Scopes]] = [global.LE]
function fun(){
// 3: fun.LE = {a,b,c,fun1,obj},并创建一个空的闭包对象fun.Closure = {}
let a = 1,b = 2,c = 3
// 4: 遇到函数,解析到函数会使用a,所以 fun.Closure={a:1,b:2}
// 5: fun1.[[Scopes]] = [global.LE, fun.Closure]
function fun1() {
debugger
console.log(a)
}
fun1()
let obj = {
// 6: 遇到函数,解析到函数会使用b,所以 fun.Closure={a:1,b:2}
// 7: method.[[Scopes]] = [global.LE, fun.Closure]
method(){
console.log(b)
}
}
}
// 执行到这里时,预编译 fun
fun()
- 对于 global.LE,不同环境下的 global.LE 内容不一样,浏览器环境下的作用域链顶层是 [window, Script],并且 script 作用域不会产生闭包对象。但是 node 环境下是 [global, Script.Closure] , node 环境下 Script 是会产生闭包的
内存泄漏
- 所谓闭包产生的内存泄漏就是因为闭包对象 Closure 无法被释放回收
- Closure 会被所有的子函数的作用域链 [[Scopes]] 引用,所以想要 Closure 不被引用就需要所有子函数都被销毁,从而导致所有子函数的 [[Scopes]] 被销毁,然后 Closure 才会被销毁
function fun(){
let arr = Array(10000000)
function fun1(){// arr 加入 Closure
console.log(arr)
}
return function fun2(){} //内部函数使用的都是同一个Closure对象
}
window.f = fun()// 长久持有fun2的引用
function fun(){
let arr = Array(10000000)
function fun1() {// arr 加入 Closure
console.log(arr)
}
window.obj = {// 长久持有 window.obj.method 的引用
method(){} //内部函数使用的都是同一个Closure对象
}
}
fun()
- 和常见闭包解释的区别,即使返回的的函数没有访问自由变量,只要有任何一个函数将 arr 添加到闭包对象 Closure 中,arr 都不会正常被销毁
查看内存泄露
- Chrome浏览器的控制台的 Performance monitor,看到 JS heap size 变化曲线不断上升并且 Memory 中手动垃圾回收的按钮后它依然没有下降到正常值,那么代码大概率是发生了内存泄漏
经典内存泄露案例
let theThing = null;
let replaceThing = function () {
let leak = theThing;
function unused() {
if (leak) {
}
}
theThing = {
longStr: new Array(1000000),
someMethod: function () {},
};
};
let index = 0;
while (index < 3) {
replaceThing();
index++;
}
console.dir(theThing);
- 比较容易发现上面代码发生内存泄漏的原因是因为 someMethod ,因为 theThing 是全局变量导致 someMethod 无法释放最终导致 replaceThing 的 Closure 无法释放
- replaceThing 的 Closure 中又存在着leak,当第一次执行时,leak为null,但继续执行时,leak指向了theThing,导致leak为null,无法释放Closure,并且当Closure存在时,反复执行会往Closure中的函数继续添加Closure
- 解决方式
- 将全局 theThing 变为 null,没有了持久引用进行解决。
- 将 leak = null,此时可以让 Closure 中的 leak 也变为 null 从而失去对 theThing 的引用,当在下一次执行 replaceThing 时会因为 theThing = xxx 导致原来的 theThing 失去最后的引用而回收掉,这也会让 theThing.someMethod 和 Closure 可以被回收
let theThing = null;
let replaceThing = function () {
let leak = theThing;
function unused () {
if (leak){}
};
theThing = {
longStr: new Array(1000000),
someMethod: function () {
}
};
leak = null // 解决问题
};
let index = 0;
while(index < 100){
replaceThing()
index++;
}
代码示例:
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script>
function t1()
{
num=100;
}
t1();
console.log(num);
function test()
{
var a=10;
function test2()
{
console.log(a);
}
test2();
}
test();
//闭包
function t2()
{
var a=10;
function t3()
{
a++;
console.log(a);
console.log(this);
}
return t3;
}
t2()();
</script>
</body>
</html>