认识内存管理

image.png

JS 内存管理

image.png

JS 垃圾回收

image.png

常见的垃圾回收算法 —— 引用计数

image.png

常见的垃圾回收算法 —— 标记清除

image.png

JS 中函数是一等公民

在JavaScript中,函数是非常重要的,并且是一等公民:

  • 那么就意味着函数的使用是非常灵活的;
  • 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;这种函数也叫作高阶函数

我们可以自己编写高阶函数

  1. function foo() { return 123; }
  2. function fn( num ) { console.log( num ) }
  3. function fa( num ) { return num }
  4. // function as a parameter
  5. fn(foo);
  6. // function as a return value
  7. fa(foo);

我们也可以使用内置的高阶函数,比如数组中的一些高阶函数

  • filter()过滤
  • map()隐射
  • forEach()迭代
  • find() / findIndex()查找 / 查找下标
  • reduce()累加

数组操作中的高级函数

filter() 过滤

filter()由数组调用,接收一个函数作为参数,这个函数有三个参数,数组元素(item),元素下标(index),数组本身(arr),返回值为布尔值。filter()的返回值为一个新数组。

执行过程:数组元素一个一个经过参数函数的处理,若是返回值为 false,则该元素不会放入新数组中,反之就会放入新数组。

  1. let arr = [1, 4, 6, 3, 7];
  2. // Filter out even elements
  3. let newArr = arr.filter( function(item) {
  4. return item % 2 === 0;
  5. })
  6. console.log( newArr ); // [4, 6]

map() 隐射

map()也和filter()一样,函数作参数,返回值为新数组。不同的地方在于功能,filter()检查每一个元素,然后过滤掉不符合要求的元素。而map()是对每一个元素进行转换,然后把转换后的元素放入新数组中,一一映射,元素个数不会少。

  1. let arr = [1, 4, 6, 3, 7];
  2. // Add 100 for each element
  3. let newArr = arr.map( function(item) {
  4. return item + 100;
  5. })
  6. console.log( newArr ); // [101, 104, 106, 103, 107]

forEach() 迭代

forEach()的参数也是一个函数,作用是一个一个迭代取出元素给参数函数处理。
注意:forEach()没有返回值,所以在参数函数中返回的结果不会被保存。手动保存当然可以。

  1. let arr = [1, 4, 6, 3, 7];
  2. let result = [];
  3. // Add 100 for each element
  4. let newArr = arr.forEach( function(item) {
  5. result.push(item + 100);
  6. return item + 100;
  7. })
  8. console.log( newArr ); // undefined
  9. console.log( arr ); // [1, 4, 6, 3, 7]
  10. console.log( result ); // [101, 104, 106, 103, 107]

find() / findIndex() 查找 / 查找下标

find()的参数也是函数,它将数组元素一个一个和参数函数中的条件比对,参数函数返回 true,则find()返回该元素。findIndex()则是返回该数组元素的下标。

  1. let person = [
  2. { name: 'lisi', age: 18 },
  3. { name: 'zhangsan', age: 19 },
  4. { name: 'wangwu', age: 20 },
  5. ];
  6. let who = person.find( function(item) {
  7. return item.age === 19;
  8. })
  9. let whoIndex = person.findIndex( function(item) {
  10. return item.age === 20;
  11. })
  12. console.log(who);
  13. console.log(whoIndex);

reduce() 累加

reduce()的参数也是函数,元素也会一个一个进入参数函数被处理,但是注意,参数函数的参数除了传统 item,index,arr 外,还有一个上一次执行参数函数的返回值 preReturnValue。并且可以初始化参数函数第一次执行的返回值。

  1. let arr = [1, 4, 6, 3, 7];
  2. // Accumulate the elements
  3. let total = arr.reduce(function(preReturnValue, item) {
  4. return preReturnValue + item;
  5. },1) // Initialization First return value to 1
  6. 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. 编译阶段:
  2. 1. 词法分析扫描代码,在堆内存中生成 GOmakeAdderadd10 均为 undefined
  3. 2. 扫描到函数体 makeAdder,堆内存中生成函数对象(包含:父级作用域GO + 函数体),函数对象中父级作用域指向 GO
  4. 3. GO 中属性 makeAdder 指向函数对象(makeAdder)
  5. 执行阶段:
  6. 1. 全局执行上下文 GEC 进入调用栈 ECSGEC VO 指向 GO
  7. 2. GEC 中代码块代码依次执行,执行到函数 makeAdder(10)
  8. 3. 生成函数执行上下文 FEC(makeAdder) 进入调用栈,准备执行
  9. 4. 堆内存中生成对应的 AO(makeAdder)(包含:count10addundefined),FEC VO 指向 AO(makeAdder)
  10. 5. 扫描到函数体 add,堆内存中生成函数对象(add)(包含:父级作用域AO(makeAdder) + 函数体),父级作用域指向 AO
  11. 6. AO 中属性 add 指向函数对象(add)
  12. 7. 准备执行阶段完成,开始执行第8行,GO 中属性 add10 指向函数对象(add)
  13. ![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")
  14. 8. 此时函数 makerAdder 执行完毕,FEC(makeAdder) 出栈
  15. - **按道理 makerAdder 函数执行完毕,它对应的 AO 函数对象不再可达,应该被销毁**
  16. - **但是现在 GO 中的属性 add10 指向了函数对象(add),AO(makerAdder) 又可达了。**
  17. 9. GEC 中执行到第9行,开始准备执行 add10(5) 函数,FEC(add10)入栈
  18. 10. 堆内存中生成 AO(add10)(包含:num5),FEC(add10) VO 指向 AO(add10)
  19. 11. 开始执行 add10(5) 函数,根据作用域链查找 num count
  20. - num 在自身AO(add10) 中找到
  21. - count 在父级作用域指向的 AO(makeAdder) 中找到
  22. ![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)
  23. 12. 函数 add10(5) 执行完毕,FEC(add10) 出栈,AO(add10) 被销毁,此时AO(makeAdder) 函数对象(add10) 仍未销毁
  24. 13. 所有代码执行完,GEC 出栈,所有分配的空间销毁。
  25. <a name="haVdv"></a>
  26. ## 闭包可能导致内存泄漏
  27. 垃圾不会被回收才是内存泄漏,那什么是垃圾呢?在 GC 算法中认为,不可达的就是垃圾,可闭包导致的 AO 内存不回收,就是因为它是可达的,所以从 GC 的角度 AO 压根就不是垃圾,闭包也不会导致内存泄漏。这也是有些人认为闭包不导致内存泄漏的原因。
  28. 但存在这么一种场景,add **函数只需要执行一次**,AO 中的自由变量 count 只要访问一次就不用了。可是 AO(makeAdder) 一直会存在内存中。对于开发者的角度来讲,这就是垃圾,内存泄漏了。这就是为什么说闭包**可能**导致内存泄漏了。
  29. 其实应该说**内存可能导致内存浪费**更贴切一点。
  30. 还有一点需要注意:像这样复制两次,fn fm 不会共用一个 bar AO bar 函数对象,而是一人一套。也就是**函数执行多少次,就会有多少个对应的 AO 函数对象**。
  31. ```javascript
  32. function foo() {
  33. let name = 'zs';
  34. return function bar() {
  35. debugger
  36. console.log(name);
  37. }
  38. }
  39. let fn = foo();
  40. let fm = foo();
  41. fn();
  42. fm();

避免闭包产生的内存浪费

避免浪费就是不用了这个函数后手动释放内存,GC 通过判断可不可达来回收内存,所以我们只要认为将该函数的 AO 改成不可达就能被 GC 认作是垃圾回收了。

对于上面的例子,手动断开 AO(makeAdder) 的连接:**add10 = null**
当 makeAdder 函数执行完出栈,AO(makeAdder) 和 函数对象(makeAdder) 相互连接但与根断开,形式孤岛被 GC 回收。

AO 中未被闭包使用的变量会被释放

  1. function foo() {
  2. let name = 'zs';
  3. let age = 18;
  4. return function bar() {
  5. debugger
  6. console.log(name); // 只访问 name,不访问 age
  7. }
  8. }
  9. let fn = foo();
  10. fn();

查看浏览器的调试,可以看到闭包中只有 name 属性。按照 ECMA 的规范,AO 没被销毁,里面应该保存该函数所有的变量。但是 V8 引擎优化了这一点,只会保存闭包使用到的变量,其他变量会在 AO 中被释放。
image.png