【JavaScript】二十九、垃圾回收 + 闭包 + 变量提升
文章目录
- 1、作用域
- 1.1 局部作用域
- 1.2 全局作用域
- 1.3 作用域链
- 2、JC垃圾回收机制♻️
- 3、GC算法
- 3.1 引用计数法
- 3.2 标记清除法
- 4、闭包
- 4.1 定义
- 4.2 闭包的应用:实现数据的私有
- 5、变量提升
1、作用域
即一个范围,离开了这个范围,这个变量就不能再被访问到
1.1 局部作用域
又分为:
- 函数作用域
- 块作用域
函数作用域:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的形参也是函数内部的局部变量
块作用域:
JS中,使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将有可能无法被访问
- let 声明的变量会产生块作用域,var 不会产生块作用域
- const 声明的常量也会产生块作用域
- 不同代码块之间的变量无法互相访问
- 推荐使用 let 或 const
1.2 全局作用域
即script 标签 和 .js 文件 的 最外层声明的变量
- 为 window 对象动态添加的属性默认也是全局的,不推荐!
- 函数中未使用任何关键字声明的变量为全局变量,不推荐!
<body><script>function m1() {num = 10}// 调用一下m1()// 成功拿到了10console.log(num)</script>
</body>
<body><script>window.myVar = '自定义'// 自定义console.log(window.myVar)</script>
</body>
1.3 作用域链
输出结果:2
作用域链本质上是底层的变量查找机制
- 在函数被执行时,会优先查找当前函数作用域中查找变量(就近)
- 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,上面例子中,就存在g函数作用域 --> f函数作用域 --> 全局作用域这个链路
2、JC垃圾回收机制♻️
整个过程:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存,即GC
<body><script>// 为变量分配内存const i = 1const str = 'test'// 为对象分配内存const obj = {uname: 'tom',age: 18}// 为函数分配内存function sum(a, b) {return a + b} </script>
</body>
- 全局变量一般不会回收
(关闭页面回收)
- 一般情况下局部变量的值, 不用了, 会被自动回收掉
- 如果不再用到的内存,没有及时释放,就叫做内存泄漏
3、GC算法
3.1 引用计数法
IE采用的引用计数算法, 定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象 算法:
- 跟踪记录被引用的次数
- 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
- 如果减少一个引用就减1 –
- 如果引用次数是0 ,则释放内存
// 引用+1
const arr = [1, 2, 3, 4]
// 引用-1
arr = null
该算法的缺陷时,出现循环引用时,会导致内存泄漏
如上,即使执行o1 = null 和 o2 = null,o1和o2也不会被回收,因为引用还剩1
3.2 标记清除法
现代浏览器通用的大多是基于标记清除算法的某些改进算法,核心思路:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收
对引用计数的循环引用问题,标记清除算法就不会有这个问题,和上图类似,执行o1 = null 和 o2 = null后,o1和o2就是一个不可达的状态了
4、闭包
4.1 定义
一个函数对周围状态的引用捆绑在一起,且内层函数中访问到其外层函数的作用域,就是闭包,简单说:闭包 = 内层函数 + 外层函数的变量,如下,内层函数f访问了外层函数outer的变量a,因此,内层函数和外层函数的变量a形成闭包
<body><script>function outer() {const a = 1function f() {console.log(a)}f()}outer()</script>
</body>
打个断点,然后刷新页面,卡在断点出后,可以看到作用域Scope里有个Closure,即闭包
闭包的作用:封闭数据,提供操作,外部也可以访问到函数内部的变量
<body><script>function outer() {const a = 1// 定义一个函数function f() {console.log(a)}// 返回一个函数,不是调用,加了小括号的f()是调用// 调用outer,就返回函数freturn f}// 函数const fun = outer()// 调用这个函数,就在外部拿到了outer函数内部的变量// 打印1fun()</script>
</body>
4.2 闭包的应用:实现数据的私有
实现统计函数的调用次数:
<body><script>let i = 0function fn() {console.log(++i)}</script>
</body>
能实现功能,但这个i 是个全局变量,很容易被修改
改一下,用局部变量和闭包:
<body><script>function count() {let i = 0function fn() {// 这里当然也可以简写成console.log(++i)i++console.log(i)}return fn}const fun = count()</script>
</body>
这样就实现了数据私有,无法直接修改,当然你别说这调用fun函数不还是修改了,这里体现了是不能被直接随意修改,fun函数里,你可以自定义权限校验代码,允许条件满足时再改
可以看出,局部变量i即使函数执行完也没有被回收,这也契合了标记清除算法的原理:从根部对象(JS的全局对象)出发,有一条引用链可达,因此不被回收,那既然这样,也就看出了闭包的另一个风险:可能导致内存泄漏(因为全局变量页面关闭时回收,那就可能存在一条一直可达的引用链)
5、变量提升
变量提升是JS中比较奇怪的现象,它允许在变量声明之前即被访问,当然仅存在于var声明变量
<body><script>// undefinedconsole.log(num)var num = 10</script>
</body>
<body><script>// 报错 Cannot access 'num' before initializationconsole.log(num)let num = 10</script>
</body>
变量提升,是指在代码执行前,内部会把当前作用域下,所有var声明的变量,提到当前作用域的最前面,只提升声明的代码,不提升赋值的代码,因此,就出现了上面的undefined,上面的代码,在变量提升后,其实就是下面的代码:
<body><script>// undefinedvar numconsole.log(num)num = 10</script>
</body>
JS变量提升的过程:
- 先把var 变量提升到当前作用域于最前面
- 只提升变量声明, 不提升变量赋值
- 然后依次执行代码
如下,变量提升,是提到当前作用域的前面,var num的作用域是fn函数,而这里是全局作用域
<body><script>function fn() {// undefinedconsole.log(num)var num = 10}fn()// 这里就会报错了,num is not defined// 变量提升,是提到本作用域的前面,var num的作用域是fn函数,而这里是全局作用域console.log(num)</script>
</body>
总结:
- 变量在未声明即被访问时会报语法错误,但var声明的变量,在声明之前即被访问,变量的值为 undefined,并不会报错
- let/const 声明的变量不存在变量提升
- 变量提升出现在相同作用域当中
- 当然,实际开发推荐先定义再使用,不建议使用var声明变量