共计 3613 个字符,预计需要花费 10 分钟才能阅读完成。
javacript 中的内存治理
javascript 中不须要咱们手动去分配内存,当咱们创立变量的时候,会主动给咱们分配内存。
- 创立根本数据类型时,会在栈内存中开拓空间寄存变量
- 创立援用数据类型时,会在堆内存中开拓空间保留援用数据类型,并将堆内存中该数据的指针返回供变量援用
var name = "alice"
var user = {
name: "kiki",
age: 16
}
申明两个不同类型变量在内存中的表现形式如下
垃圾回收机制
内存是无限的,当某些内存不须要应用的时候,咱们须要对其开释,以腾出更多的内存空间,在 javascript 中有两种垃圾回收算法。
1. 援用计数
当对象有援用指向它的时候,计数减少 1,打消指向就缩小 1,当计数为 0 时,对象会主动被垃圾回收器及销毁
这样的回收机制可能存在问题,当两个对象循环援用时,这两个对象都不会被销毁,可能存在内存透露的状况
2. 标记革除
设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有援用到的对象,销毁没有援用到的对象
这样一种算法能够比拟无效的解决循环援用的问题,下图中 MN 从根节点中无奈找到有援用的对象,所以会被垃圾回收器销毁
函数的多种用处
在 javascript 中,函数是十分重要且利用宽泛的,它最罕用有以下几种
1、作为参数传递
函数能够间接作为另一个函数的参数,并且间接调用执行,以下定义了多种计算数字的办法,加减乘,进行不同的运算不须要每次调用不同的函数,只须要扭转传参即可
function calcNum(num1, num2, fn) {console.log(fn(num1, num2))
}
function add(num1, num2) {return num1 + num2}
function minus(num1, num2) {return num1 - num2}
function mul(num1, num2) {return num1 * num2}
calcNum(10, 20, add) // 30
2、作为返回值
函数也能够返回一个函数,以下函数叫做高阶函数,也成为函数柯里化,能够屡次接管返回并进行对立的解决
function makeAdder(count) {function add(num) {return count + num}
return add
}
var add5 = makeAdder(5)
console.log(add5(6))
console.log(add5(10))
var add10 = makeAdder(10)
var add100 = makeAdder(100)
3、作为回调函数
在数组中有很多办法都须要咱们自定义回调函数来解决数据
var nums = [10, 50, 20, 100, 40]
var newNums = nums.map(function(item){return item * 10})
闭包
如果一个函数,能够拜访到外层的自在变量,那么它就是闭包
如以下代码所示,bar 函数能够拜访到父级作用域的变量 name 和 age
function foo(){
var name = "foo"
var age = 18
function bar(){console.log(name)
console.log(age)
}
return bar
}
var fn = foo()
fn()
以上代码执行后果为
foo
18
依照代码的执行程序来说,foo 函数被执行实现,它的函数上下文曾经从栈中弹出,而 foo 函数中的变量为什么还能被保留下来?
因为 foo 函数执行上下文创立的时候,同时创立 AO 对象,AO 对象依然被 bar 函数的 parentScope 所指向,所以不会被垃圾回收器销毁
以上代码在内存中的执行过程如下
-
Javascript –> AST
- 在内存中开拓空间 0x100 保留函数 foo,其中保留父级作用域(parentScope)指向 GO
- 内存中存在 GO(Global Object)对象,其中包含了内置的模块如 String、Number 等,同时将定义的全局变量保留至 GO 中,这里将 fn 增加到 GO 中,值为 undefined,函数 foo 增加到 GO 中,值为 0x100
-
Ignition 解决 AST
- V8 引擎执行代码时,存在调用栈 ECStack,创立全局执行上下文,VO 指向 GO
- 创立函数 foo 的函数执行上下文,**VO(variable object)指向 foo 的 AO(
active object)**,执行函数体内代码 - 创立 foo 的 AO 对象,将 name 和 age 增加到 AO 中,值为 undefined,
- 执行代码前,给 foo 内的函数 bar 开拓内存空间 0x200,bar 函数的父级作用域指向 AO
- 将 foo 增加到 AO 对象中,值为 0x200
-
执行代码
- 函数 foo 的返回值 bar 函数赋值给 fn,所以fn 的值为 bar 函数的内存地址 0x200
- 执行 foo 函数,将 foo 的 AO 对象中的 name 赋值为 foo,age 赋值为 18
- 执行 fn 函数前,bar 函数创立函数执行上下文,VO 指向 bar 的 AO
- 创立 bar 的 AO,bar 函数内没有定义变量,所以 AO 为空
- 执行 fn 函数,输出 name 和 age
-
执行实现
- foo 函数被执行实现,foo 函数的执行上下文弹出调用栈
- bar 函数被执行实现,bar 函数的执行上下文弹出调用栈
- bar 的 AO 对象是函数执行上下文存在时创立,此时也没有被其它中央援用,所以会被垃圾回收器销毁
- bar 函数赋值给了全局变量,不会被销毁,并且 bar 的父级作用域指向 foo 的 AO 对象,因而foo 的 AO 对象也不会被销毁,所以在 bar 函数中能拜访到 foo 中的变量
图示如下
AO 优化
以上 foo 的 AO 对象有被援用,所以没有销毁,如果此时 AO 对象只是局部变量被援用,而其它变量没有用到呢,那没有用到的变量会被销毁吗?比方以下 foo 函数的变量 age
function foo(){
var name = "foo"
var age = 18
return function(){console.log(name)
}
}
var fn = foo()
fn()
依照 ECMAScript 标准是不会的,因为整个 AO 对象都被保留在内存中了,然而 JS 引擎可能会做一些优化,比如说 Chome 浏览器应用的 V8 引擎
在以上闭包中减少 debugger 进行调试
function foo(){
var name = "foo"
var age = 18
return function(){
debugger
console.log(name)
}
}
var fn = foo()
fn()
能够用两种办法测试到 foo 的变量 age 没有被保留下来
1. 在 Sources 中查看 Closure 保留的变量
代码执行到 debugger 处,能够查看到闭包此时的作用域,父级作用域 foo 中只保留了变量 name
2. 在 Console 中输入变量
当代码执行到 debugger 处,此时 Console 就是在闭包的执行环境中,能够间接打印变量,name 能够间接被打印进去,而打印 age 则间接保留未定义
内存透露
如上述例子中被保留到全局的闭包,因为有相互的援用,不会被销毁,如果后续不再应用,就可能呈现内存透露的状况。
用以下代码测试一下
function createFnArray() {var arr = new Array(1024 * 1024).fill(1)
return function () {console.log(arr.length)
}
}
var arrayFns = []
for (var i = 0; i < 100; i++) {setTimeout(() => {arrayFns.push(createFnArray())
}, i * 100)
}
setTimeout(() => {for (var i = 0; i < 50; i++) {setTimeout(() => {arrayFns.pop()
}, 100 * i)
}
}, 10000)
以上代码每隔 0.1s 创立一个内存容量为 1024*1024 的数组(约 4M)保留到全局变量中,共计 100 个,再隔 10s 后将每隔 0.1s 从数组底部弹出一个元素,共计 50 个。
这样操作在内存中的体现应为前 10s 陆续减少内存的应用,第 10s 时,内存占用约为 400M,等到 15s 后,内存占用缩小一半,因为垃圾回收器不会马上回收或销毁垃圾,所以可能会有肯定的时间延缓
开释内存
内存的大量占用会造成内存透露,当不须要应用的时候,要及时的开释,只须要将变量指向 null,即可开释内存
var fn = foo()
// 无需应用时
fn = null
以上就是对于内存和闭包的了解,对于 js 高级,还有很多须要开发者把握的中央,能够看看我写的其余博文,继续更新中~