垃圾回收?
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题,但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏的情况。
什么是内存泄露: 不是指某一块内存真的泄露掉了,而是指某一块内存无法被回收
内存的生命周期:
JS环境中分配的内存, 一般有如下生命周期:
- 内存分配
- 内存使用
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存。全局变量一般不会回收, 一般局部变量的的值不用了, 会被自动回收掉
// 为变量分配内存
let i = 11
let s = "ifcode"
// 为对象分配内存
let person = {
age: 22,
name: 'ifcode'
}
// 为函数分配内存
function sum(a, b) {
return a + b;
}
两种垃圾回收算法:
所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉
下面介绍两种常见的浏览器垃圾回收算法: 引用计数 和 标记清除法
引用计数:
IE采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
如果没有任何变量指向它了,说明该对象已经不再需要了。
// 创建一个对象person, person指向一块内存空间, 该内存空间的引用数 +1
let person = {
age: 22,
name: 'ifcode'
}
let p = person // 两个变量指向一块内存空间, 该内存空间的引用数为 2
person = 1 // 原来的person对象被赋值为1,对象内存空间的引用数-1,
// 但因为p指向原person对象,还剩一个对于对象空间的引用, 所以对象它不会被回收
p = null // 原person对象已经没有引用,会被回收
由上面可以看出,引用计数算法是个简单有效的算法。
但它却存在一个致命的问题:循环引用。
如果两个对象相互引用,尽管函数执行完毕,它们已不再使用,垃圾回收器不会进行回收,导致内存泄露
function cycle() {
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()
函数执行完毕之后,变量 o1 和 o2 分配的内存会被回收,但是由于堆中原先开辟的空间还有人在“用”,所以不会被回收。虽然在“用”,你会发现压根就没办法用到它们,所以就会导致那块内存没有被回收,就会导致内存泄漏
标记清除算法:
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
- 凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念。同时,对于上面那个循环引用的例子,可以发现,函数执行完毕之后,堆中两块内存虽然相互使用,但是从根部到达不了,所以也回收
如何让某块空间不被回收:
根据标记清除算法的定义,某一块空间不想被回收,那么就让它变成可到达
function fn(){
const a = 10
const obj = {}
console.log(a)
}
fn()
这段代码中 fn() 一旦执行完毕,fn() 及里面的东西都会被回收;为了让某块空间不被回收,可以这么干:
function fn(){
const a = 10
const obj = {} // 注意,obj存放的是地址
console.log(a)
return obj
}
const res = fn()
在 fn() 中 return 出来 obj 存放的地址,这块地址现在被 res 使用着,所以原先 obj 指向的那块地址就不会被回收
注意,不会被回收的重点不是在 return 出来一块地址,而是在于有人用着这块地址
从垃圾回收看 Vue:
前置知识:JS 中创建一个对象,其实是开辟一块新空间
在 Vue 中,在声明某一个组件内的数据时,会这么干:
<script>
export default {
data(){
return {
name:'zs'
}
}
}
</script>
那么,为什么 data 内是要以 return 的形式,而不是直接写成:
<script>
export default {
data{
name:'zs'
}
}
</script>
第一个,和垃圾回收有关:
前者的写法 return 出去一个对象(应该说 return 出去一个地址合适),就能保证组件执行完毕之后不会被回收(因为存放数据的这块地址仍然可到达)
第二个,和创建对象时是开辟一块新空间有关:
const obj1 = {
name:'zs'
}
const obj2 = {
name:'zs'
}
在这段代码中,虽然 obj1 和 obj2 都是创建了一个含有 name 字段且值为'zs'的对象,但是它们两个仍然是不同的。因为在创建对象时实际上 obj1 和 obj2 拿到的是不同的地址值
回到 Vue 中来,我们说 data 采用 return 出去一个对象的写法还有一个作用就是:
每次 return 的对象的地址都是不同的,这样就能保证组件之间不会使用同一份地址,也就是不同组件不会使用同一份数据
JS 的作用域链:
作用域:
首先讲讲作用域
作用域可分为局部作用域和全局作用域,这一概念其实和 C 语言或是 Java 中的概念是一致的:局部作用域内的东西只能局部用(如函数内的变量只能在函数内部使用);全局作用域全局可以使用(如全局变量函数内还是可以使用)
作用域链:
有没有想过这种“函数内的变量只能在函数内部使用,全局变量函数内还是可以使用”是为什么?答案就是作用域链
官方概念: JavaScript 在执⾏过程中会创建一个个的可执⾏上下⽂(每个函数执行都会创建这么一个可执行上下文)。每个可执⾏上下⽂的词法环境中包含了对外部词法环境的引⽤,可通过该引⽤来获取外部词法环境中的变量和声明等。这些引⽤串联起来,⼀直指向全局的词法环境,形成一个链式结构,被称为作⽤域链。
太过官方,简单来讲,就是: 函数内部可以访问到函数外部作用域的变量, 而外部函数还可以访问到全局作用域的变量,这样的变量作用域访问的链式结构, 被称之为作用域链
<script>
const count = 100
function outer(){
const num = 10
// 可以访问全局变量,但不能访问内部函数的变量
function inner(){
// 可以访问外部函数、全局变量
}
}
</script>
这一点其实和 C 语言中的基本一致,主要是要搞清楚什么是词法环境?什么是可执行上下文?什么又是链式?
下图为由多个可执行上下文组成的调用栈:
- 栈最底部为
全局可执行上下文
全局可执行上下文
之上有多个函数可执行上下文
- 每个可执行上下文中包含了指向外部其他可执行上下文的引用,直到
全局可执行上下文
时它指向null
回顾栈的特点:先进后出。还是下面这段代码:
<script>
const count = 100
function outer(){
const num = 10
// 可以访问全局变量,但不能访问内部函数的变量
function inner(){
// 可以访问外部函数、全局变量
}
}
</script>
首先先执行全局的内容,此时全局上下文进栈(里面有全局变量 count);执行 outer(),outer()进栈(里面有变量 num);执行 inner(),inner()进栈,大致形成下图的模样:
其实这里顺带可以和垃圾回收机制联系起来:
假设现在已经执行完了 outer(),那么就会被浏览器回收掉。既然回收掉了,那么全局也就无法再访问到原先 outer() 内的变量了
闭包:
什么是闭包?闭包的作用是什么?闭包和垃圾回收以及作用域链怎么联系起来?
什么是闭包?
MDN的官方解释:闭包是函数和声明该函数的词法环境的组合
简单理解就是:内层函数, 引用外层函数上的变量, 就可以形成闭包
需求: 定义一个计数器方法, 每次执行一次函数, 就调用一次进行计数
let count = 0
function fn () {
count++
console.log('fn函数被调用了' + count + '次')
}
fn()
但是这种方案存在一个弊端:count 是全局变量,在哪里都可以修改它,这就导致 count 并不是私有的
为什么 count 是全局变量就会导致在哪里都可以修改它? 作用域链的知识
某些场景下为了让某些变量实现私有,就可以使用闭包
所以闭包的最大作用就是用来实现 变量私有 的
既然讲了闭包是:内层函数引用外层函数变量,所以可将上述代码改造成:
function fn () {
let count = 0
function inner(){
count++
console.log('fn函数被调用了' + count + '次')
}
return inner
}
const res = fn()
res() // count = 1
res() // count = 2
res() // count = 3
res() // count = 4
inner() 内层函数调用了外层函数的 count 变量,所以现在在函数外部就不能改变 count 的值了(作用域链)
那么在 fn() 执行完毕之后 fn() 内的所有东东不会被回收掉吗?不会,因为 fn() return 出来了 inner 给 res,相当于根部仍然能够走到 inner 这块地址上,所以 inner 不会被回收;而 inner 还使用着 count,所以 count 也不会被回收(垃圾回收机制)
讲到这里,其实你发现了闭包就已经和作用域链以及垃圾回收机制串起来了,所以它们三者是相互联系的
会被垃圾回收机制回收的执行方式:
function fn() {
let count = 0
function inner() {
count++
console.log('fn函数被调用了' + count + '次')
}
return inner
}
const res = fn()
// 会被回收的执行方式
fn()() // 执行完这行代码之后被回收
fn()() // 执行完这行代码之后被回收
打印结果:
你会发现 count 似乎没有实现自增;原因就是每一次执行完 fn()(),这块地址已经变得不可到达,就会被回收;等到下一次再来执行 fn()() ,就是使用了新的 count 值了
为什么要让 fn() 内的 inner() 那一块地址不被回收?
还是以上面的代码为例,假如执行一次,就回收一次,那么执行完一次,下次再次执行时又会开辟新空间,而 count 又会被初始化为 0,这显然与“ 每次执行一次函数, 就调用一次进行计数”相违背