浅谈闭包、作用域和垃圾回收
在网上的很多关于闭包的文章,但是我看了这么多的文章之后对闭包的概念还是迷迷糊糊的,虽然别人问起来可以向别人解释什么是闭包(当然只是浅显的解释),但是脑子里对闭包产生的根本原因还是有疑惑,后来看了垃圾回收的文章和作用域的文章之后才完全理解透。
闭包
关于闭包的定义,我的理解是一个函数访问了它自己的作用域以外的变量就是闭包。我现在的理解闭包就是一个名字而已,归根到底还是作用域和垃圾回收的问题。
官方解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
下面是分析为什么产生闭包。
JS的垃圾回收机制
先贴文章:
首先,JS的内存管理是自动执行的,创建对象、回收对象这些工作都是不用我们去操作的,然后,垃圾回收的标准就是这个对象是否可达(可达性)。
可达性
可达性我的理解就是:在window
这个大作用域下,能不能通过这种引用的指针找到这个对象/值。
例如:
var a = {name: 'a'}
var b = {
name: 'b',
child: a
}
a = null
console.log(b)
b.child = null
声明了a
、b
两个变量之后:
a
指向null
之后:
b.child
指向null
之后:
这时候
{name: 'a'}
这个对象就是处于不可达状态,关于垃圾回收器是如何判断哪些对象不可达的垃圾回收算法,在上面的第一篇链接有说明,是通过标记-清除来找到不可达的对象。
作用域
我先用非常普通的例子来说明:
// var声明的变量会提升
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
会输出5个5,这个例子也非常常见易懂,因为var
声明会提升,不存在块级作用域的说法,这段代码var
能上升到的作用域就是window
的作用域,所以setTimeout
的回调函数访问的i是window
的i
,是全局变量,for循环改变的是全局变量所以就会输出5个5。
catch
的作用域
接下来的例子是try/catch中的catch作用域:
for (var i = 0; i < 5; i++) {
try {
throw i
} catch (i) {
var j = i
setTimeout(function () {
console.log(i, j)
}, 0)
}
}
输出的结果是:
0 4
1 4
2 4
3 4
4 4
catch
里面的作用域就比较特别,如果是通过形参的形式传过来的变量,它在catch
里面有自己的作用域,也就是catch
以外就不能够访问到它,但是在catch
里面用var
定义的变量,他就回提升,而且在这个例子里面,他能够提升到的最高作用域是window
。
当上面的循环结束之后:
当回调函数1被推入执行栈时执行
console.log(i, j)
,变量i
是通过作用域链往上一级找到了i
,j
是往上两级找到了j
。因为回调函数会回到全局的环境当中,i
是有路可以到达的,所以js保存了i
的作用域,没有被回收。而j
作为全局变量,在循环里改变的是全局的变量的值,所以输出的全是4
。
为什么闭包问题都会说到函数上,我认为是在js里面只有函数的作用域才正真是块级作用域,在 js 的函数当中,存在一个[[scope]]
属性,这个属性就是用来存放当前函数上下文的全部对象,这个就是函数的作用域。
在函数里面定义的变量或函数都不可能会被提升到函数作用域外的地方,就是如果不把引用暴露出去,是没办法获取到这些在函数里面定义的变量或函数。
又是一道网上常见的闭包:
function person () {
var name = '张三'
function nameGetter () {
return name
}
return nameGetter
}
var getName = person()
console.log(name) // 报 name is not defined 错
console.log(getName()) // 张三
因为闭包的作用在网上很多文章都有说,所以这里就直接分析内存情况:
刚定义person
的时候:
person
调用完之后:(这边把person
改成立即执行函数先)
var getName = (function () {
var name = '张三'
function nameGetter () {
return name
}
return nameGetter
})()
内存情况:
一般立即执行函数/匿名函数调用结束后就会处于不可达状态,因为没有指针指向它们,空间会被回收,但是像这种情况就是闭包,js引擎可以沿着全局的
getName
指针找到对应函数的在堆的位置,在函数里面又可以沿着找到name
对应的值,所以,在匿名函数用var
定义的变量是处于可达状态,可以被标记,所以垃圾回收器不会去回收它,但是他不能通过全局作用域直接访问,必须沿着沿着链表去找到,所以利用闭包可以定义私有属性。
总结
闭包的产生是变量的作用域和js的垃圾回收机制的共同结果。因为一般产生闭包的作用域是函数,函数又比较大,所以容易产生内存溢出的问题,而且变量一直处于可达状态,打开浏览器的时间久了,页面就会出现内存不足而卡顿的情况,设置指针为null
就可以使原来指向的对象处于不可达状态,js垃圾回收器在一段时间内就会去回收。
转载自:https://juejin.cn/post/6844903928421679118