认识内存管理
JS 内存管理
JS 垃圾回收
常见的垃圾回收算法 —— 引用计数
常见的垃圾回收算法 —— 标记清除
JS 中函数是一等公民
在JavaScript中,函数是非常重要的,并且是一等公民:
- 那么就意味着函数的使用是非常灵活的;
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;这种函数也叫作高阶函数
我们可以自己编写高阶函数
function foo() { return 123; }
function fn( num ) { console.log( num ) }
function fa( num ) { return num }
// function as a parameter
fn(foo);
// function as a return value
fa(foo);
我们也可以使用内置的高阶函数,比如数组中的一些高阶函数
filter()
过滤map()
隐射forEach()
迭代find() / findIndex()
查找 / 查找下标reduce()
累加
数组操作中的高级函数
filter() 过滤
filter()
由数组调用,接收一个函数作为参数,这个函数有三个参数,数组元素(item),元素下标(index),数组本身(arr),返回值为布尔值。filter()
的返回值为一个新数组。
执行过程:数组元素一个一个经过参数函数的处理,若是返回值为 false,则该元素不会放入新数组中,反之就会放入新数组。
let arr = [1, 4, 6, 3, 7];
// Filter out even elements
let newArr = arr.filter( function(item) {
return item % 2 === 0;
})
console.log( newArr ); // [4, 6]
map() 隐射
map()
也和filter()
一样,函数作参数,返回值为新数组。不同的地方在于功能,filter()
检查每一个元素,然后过滤掉不符合要求的元素。而map()
是对每一个元素进行转换,然后把转换后的元素放入新数组中,一一映射,元素个数不会少。
let arr = [1, 4, 6, 3, 7];
// Add 100 for each element
let newArr = arr.map( function(item) {
return item + 100;
})
console.log( newArr ); // [101, 104, 106, 103, 107]
forEach() 迭代
forEach()
的参数也是一个函数,作用是一个一个迭代取出元素给参数函数处理。
注意:forEach()
没有返回值,所以在参数函数中返回的结果不会被保存。手动保存当然可以。
let arr = [1, 4, 6, 3, 7];
let result = [];
// Add 100 for each element
let newArr = arr.forEach( function(item) {
result.push(item + 100);
return item + 100;
})
console.log( newArr ); // undefined
console.log( arr ); // [1, 4, 6, 3, 7]
console.log( result ); // [101, 104, 106, 103, 107]
find() / findIndex() 查找 / 查找下标
find()
的参数也是函数,它将数组元素一个一个和参数函数中的条件比对,参数函数返回 true,则find()
返回该元素。findIndex()
则是返回该数组元素的下标。
let person = [
{ name: 'lisi', age: 18 },
{ name: 'zhangsan', age: 19 },
{ name: 'wangwu', age: 20 },
];
let who = person.find( function(item) {
return item.age === 19;
})
let whoIndex = person.findIndex( function(item) {
return item.age === 20;
})
console.log(who);
console.log(whoIndex);
reduce() 累加
reduce()
的参数也是函数,元素也会一个一个进入参数函数被处理,但是注意,参数函数的参数除了传统 item,index,arr 外,还有一个上一次执行参数函数的返回值 preReturnValue。并且可以初始化参数函数第一次执行的返回值。
let arr = [1, 4, 6, 3, 7];
// Accumulate the elements
let total = arr.reduce(function(preReturnValue, item) {
return preReturnValue + item;
},1) // Initialization First return value to 1
console.log(total); // 21 + 1 = 22
闭包
闭包的定义
- 闭包 = 函数本身 + 函数所访问的自由变量
闭包的执行过程
```javascript function makeAdder(count) { function add(num) { return count + num; } return add; }
var add10 = makeAdder(10); console.log(add10(5));
编译阶段:
1. 词法分析扫描代码,在堆内存中生成 GO,makeAdder,add10 均为 undefined
2. 扫描到函数体 makeAdder,堆内存中生成函数对象(包含:父级作用域GO + 函数体),函数对象中父级作用域指向 GO
3. GO 中属性 makeAdder 指向函数对象(makeAdder)
执行阶段:
1. 全局执行上下文 GEC 进入调用栈 ECS,GEC 中 VO 指向 GO
2. GEC 中代码块代码依次执行,执行到函数 makeAdder(10)
3. 生成函数执行上下文 FEC(makeAdder) 进入调用栈,准备执行
4. 堆内存中生成对应的 AO(makeAdder)(包含:count:10,add:undefined),FEC 中 VO 指向 AO(makeAdder)
5. 扫描到函数体 add,堆内存中生成函数对象(add)(包含:父级作用域AO(makeAdder) + 函数体),父级作用域指向 AO
6. AO 中属性 add 指向函数对象(add)
7. 准备执行阶段完成,开始执行第8行,GO 中属性 add10 指向函数对象(add)
![image.png](https://cdn.nlark.com/yuque/0/2022/png/22919157/1647943004361-276e874d-87eb-4c8a-9b15-689198d55803.png#clientId=u090d798d-c1bf-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=353&id=ud85ca810&margin=%5Bobject%20Object%5D&name=image.png&originHeight=441&originWidth=858&originalType=binary&ratio=1&rotation=0&showTitle=true&size=228002&status=done&style=none&taskId=u98f5c501-3134-487a-9d8b-bfe5ac63636&title=%E5%9B%BE%E4%B8%AD%E6%9C%AA%E6%A0%87%E5%87%BAAO%E4%B8%ADadd%E6%8C%87%E5%90%910xb00%20%E5%92%8C%200xa00%E7%88%B6%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8C%87%E5%90%91GO&width=686.4 "图中未标出AO中add指向0xb00 和 0xa00父级作用域指向GO")
8. 此时函数 makerAdder 执行完毕,FEC(makeAdder) 出栈
- **按道理 makerAdder 函数执行完毕,它对应的 AO 和 函数对象不再可达,应该被销毁**
- **但是现在 GO 中的属性 add10 指向了函数对象(add),AO(makerAdder) 又可达了。**
9. GEC 中执行到第9行,开始准备执行 add10(5) 函数,FEC(add10)入栈
10. 堆内存中生成 AO(add10)(包含:num:5),FEC(add10) 中 VO 指向 AO(add10)
11. 开始执行 add10(5) 函数,根据作用域链查找 num 和 count。
- num 在自身AO(add10) 中找到
- count 在父级作用域指向的 AO(makeAdder) 中找到
![image.png](https://cdn.nlark.com/yuque/0/2022/png/22919157/1647946216786-b6a5741c-7d62-4a2d-9f8f-63aea070fd70.png#clientId=u090d798d-c1bf-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=330&id=u6ed60c54&margin=%5Bobject%20Object%5D&name=image.png&originHeight=412&originWidth=728&originalType=binary&ratio=1&rotation=0&showTitle=false&size=173778&status=done&style=none&taskId=u3407b24f-2a7f-4991-b870-3c8ff09f482&title=&width=582.4)
12. 函数 add10(5) 执行完毕,FEC(add10) 出栈,AO(add10) 被销毁,此时AO(makeAdder) 和 函数对象(add10) 仍未销毁
13. 所有代码执行完,GEC 出栈,所有分配的空间销毁。
<a name="haVdv"></a>
## 闭包可能导致内存泄漏
垃圾不会被回收才是内存泄漏,那什么是垃圾呢?在 GC 算法中认为,不可达的就是垃圾,可闭包导致的 AO 内存不回收,就是因为它是可达的,所以从 GC 的角度 AO 压根就不是垃圾,闭包也不会导致内存泄漏。这也是有些人认为闭包不导致内存泄漏的原因。
但存在这么一种场景,add **函数只需要执行一次**,AO 中的自由变量 count 只要访问一次就不用了。可是 AO(makeAdder) 一直会存在内存中。对于开发者的角度来讲,这就是垃圾,内存泄漏了。这就是为什么说闭包**可能**导致内存泄漏了。
其实应该说**内存可能导致内存浪费**更贴切一点。
还有一点需要注意:像这样复制两次,fn 和 fm 不会共用一个 bar AO 和 bar 函数对象,而是一人一套。也就是**函数执行多少次,就会有多少个对应的 AO 和 函数对象**。
```javascript
function foo() {
let name = 'zs';
return function bar() {
debugger
console.log(name);
}
}
let fn = foo();
let fm = foo();
fn();
fm();
避免闭包产生的内存浪费
避免浪费就是不用了这个函数后手动释放内存,GC 通过判断可不可达来回收内存,所以我们只要认为将该函数的 AO 改成不可达就能被 GC 认作是垃圾回收了。
对于上面的例子,手动断开 AO(makeAdder) 的连接:**add10 = null**
当 makeAdder 函数执行完出栈,AO(makeAdder) 和 函数对象(makeAdder) 相互连接但与根断开,形式孤岛被 GC 回收。
AO 中未被闭包使用的变量会被释放
function foo() {
let name = 'zs';
let age = 18;
return function bar() {
debugger
console.log(name); // 只访问 name,不访问 age
}
}
let fn = foo();
fn();
查看浏览器的调试,可以看到闭包中只有 name 属性。按照 ECMA 的规范,AO 没被销毁,里面应该保存该函数所有的变量。但是 V8 引擎优化了这一点,只会保存闭包使用到的变量,其他变量会在 AO 中被释放。