JS基础---调用堆和栈

调用堆和栈

v8引擎主要有五部分组成: 内存堆、调用栈、 web API 、事件循环(Event Loop)、回调队列

  • 内存堆:这是内存分配发生的地方。当V8引擎遇到变量声明和函数声明的时候,就把它们存储在堆里面。
  • 调用栈:这是你的代码执行时的地方。当引擎遇到像函数调用之类的可执行单元,就会把它们推入调用栈。
  • Web API:还有很多引擎之外的 API,我们把这些称为浏览器提供的 Web API,比如说 事件监听函数、DOM、HTTP/AJAX请求、setTimeout等等。
  • 事件循环:持续的检测调用栈和回调队列,如果检测到调用栈为空,它就会通知回调队列把队列中的第一个回调函数推入执行栈。
  • 回调队列:按照先进先出的顺序存储所有的回调函数。在任意时间,只要Web API容器中的事件达到触发条件,就可以把回调函数添加到回调队列中去。

JS运行时环境的工作机制:
JS引擎(唯一主线程)按顺序解析代码,遇到函数声明,直接跳过,遇到函数调用,入栈;
如果是同步函数调用,直接执行得到结果,同步函数弹出栈,继续下一个函数调用;
如果是异步函数调用,分发给Web API(多个辅助线程),异步函数弹出栈,继续下一个函数调用;
Web API中,异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了,就把回调函数推入回调队列中。
Event Loop不停地检查主线程的调用栈与回调队列,当调用栈空时,就把回调队列中的第一个任务推入栈中执行,不断循环。

调用栈

  1. 每调用一个函数,解释器就会把该函数添加进调用栈中开始执行
  2. 正在调用栈中执行的函数还调用了其它函数,那么新函数也会被添加进调用栈,一旦这个函数被调用,就会立即执行
  3. 当前函数执行完毕后,解释器将其清楚调用栈,继续执行当前执行环境下的剩余代码
  4. 当分配的调用栈空间被占满时,会发生"堆栈溢出"错误

除了广义的同步任务和异步任务,我们对任务有了更精细的定义:
宏任务: script/ setTimeout/ setInterval / Ajax/ 事件监听函数/ DOM/ (webAPI)
微任务: Promise/ process.nextTick/ async (回调队列)

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

深入理解

垃圾回收机制

算法: 引用计数(现代浏览器不再使用) 标记清除(常用)
标记清除: 即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。
最常见的内存泄露一般都与DOM元素绑定有关

email.message = document.createElement(“div”);
displayList.appendChild(email.message);
// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();
// 上面代码中,div元素已经从DOM树中清除,但是该div元素还绑定在email对象中,所以如果email对象存在,那么该div元素就会一直保存在内存中。

内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

4种常见的内存泄漏

  1. 意外的全局变量

    // 未定义的变量会在全局对象创建一个新变量
    function foo(arg) {
        bar = "this is a hidden global variable";
    }
    // 函数 foo 内部忘记使用 var ,实际上JS会把bar挂载到全局对象上,意外创建一个全局变量。
    function foo(arg) {
        window.bar = "this is an explicit global variable";
    }
    // 另一个意外的全局变量可能由 this 创建。
    // 这种可以避免, 加上严格模式 'use strict'
    function foo() {
        this.variable = "potential accidental global";
    }
    foo();// foo 调用自己,this 指向了全局对象(window)  而不是 undefined
    
  2. 被遗忘的计时器或回调函数
    计时器忘记清除
    监听的回调函数(现代浏览器使用了更先进的垃圾回收,可以正确检测和处理循环引用了)

     var element = document.getElementById('button');
     function onClick(event) {
         element.innerHTML = 'text';
     }
     element.addEventListener('click', onClick);
    
  3. 脱离 DOM 的引用
    如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。

    var elements = {
         button: document.getElementById('button'),
         image: document.getElementById('image'),
         text: document.getElementById('text')
     };
     function doStuff() {
         image.src = 'http://some.url/image';
         button.click();
         console.log(text.innerHTML);
         // 更多逻辑
     }
     function removeButton() {
         // 按钮是 body 的后代元素
         document.body.removeChild(document.getElementById('button'));
         // 此时,仍旧存在一个全局的 #button 的引用
         // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
     }
    

    如果代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。所以保存 DOM 元素引用的时候,要小心谨慎。

  4. 闭包
    闭包的关键是匿名函数可以访问父级作用域的变量。

     var theThing = null;
     var replaceThing = function () {
     var originalThing = theThing;
     var unused = function () {
         if (originalThing)
         console.log("hi");
     };
         
     theThing = {
         longStr: new Array(1000000).join('*'),
         someMethod: function () {
         console.log(someMessage);
         }
     };
     };
    
     setInterval(replaceThing, 1000);
    

    每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。
    解决办法: 在 replaceThing 的最后添加 originalThing = null 。

tip

栈/堆/池(常量)

ES6 新出的两种数据结构:WeakSet 和 WeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。

思考题

问题一

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x  // --> undefined
b.x  // --> {n: 2}

// 1、优先级。.的优先级高于=,所以先执行a.x,堆内存中的{n: 1}就会变成{n: 1, x: undefined},改变之后相应的b.x也变化了,因为指向的是同一个对象。
// 2、赋值操作是从右到左,所以先执行a = {n: 2},a的引用就被改变了,然后这个返回值又赋值给了a.x,需要注意的是这时候a.x是第一步中的{n: 1, x: undefined}那个对象,其实就是b.x,相当于b.x = {n: 2}

问题二: 从内存来看 null 和 undefined 本质的区别是什么?

变量赋值为null, 相当于这个变量的指针对象以及值清空,给对象的属性 赋值为null相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。

变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值

问题三: ES6语法中的 const 声明一个只读的常量,那为什么下面可以修改const的值?

const 保证的是指向的那个内存地址所保存的数据不得改动

问题三: 哪些情况下容易产生内存泄漏?

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值