【进阶1-5期】JavaScript深刻之4类常见内存泄漏及如何避免

(关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)前端

本周正式开始前端进阶的第一期,本周的主题是调用堆栈,今天是第5天。node

本计划一共28期,每期重点攻克一个面试重难点,若是你还不了解本进阶计划,点击查看前端进阶的破冰之旅webpack

若是以为本系列不错,欢迎转发,您的支持就是我坚持的最大动力。git

本期推荐文章

4类 JavaScript 内存泄漏及如何避免 ,因为微信不能访问外链,点击阅读原文就能够啦。es6

推荐理由

上篇文章介绍了垃圾回收机制,可是都是些概念,今日份文章(译文)有代码有讲解,详解介绍了经常使用内存泄漏并说明了如何避免,对于提高我的知识深度很是有帮助。github

阅读笔记

上篇文章详细介绍了内存回收和内存泄漏,今天咱们继续这个篇幅,不太重点是内存泄漏可能发生的缘由。没看过上篇的点击【进阶1-4期】JavaScript深刻之带你走进内存机制web

垃圾回收算法

经常使用垃圾回收算法叫作标记清除 (Mark-and-sweep) ,算法由如下几步组成:面试

  • 一、垃圾回收器建立了一个“roots”列表。roots 一般是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被看成 root 。window 对象老是存在,所以垃圾回收器能够检查它和它的全部子对象是否存在(即不是垃圾);
  • 二、全部的 roots 被检查和标记为激活(即不是垃圾)。全部的子对象也被递归地检查。从 root 开始的全部对象若是是可达的,它就不被看成垃圾。
  • 三、全部未被标记的内存会被当作垃圾,收集器如今能够释放内存,归还给操做系统了。

现代的垃圾回收器改良了算法,可是本质是相同的:可达内存被标记,其他的被看成垃圾回收。算法

四种常见的JS内存泄漏

划重点 这是个考点跨域

一、意外的全局变量

未定义的变量会在全局对象建立一个新变量,以下。

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 建立。

function foo() {
    this.variable = "potential accidental global";
}

// Foo 调用本身,this 指向了全局对象(window)
// 而不是 undefined
foo();

解决方法

在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。若是必须使用全局变量存储大量数据时,确保用完之后把它设置为 null 或者从新定义。

二、被遗忘的计时器或回调函数

计时器setInterval代码很常见

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

上面的例子代表,在节点node或者数据再也不须要时,定时器依旧指向这些数据。因此哪怕当node节点被移除后,interval 仍旧存活而且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。

var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}

element.addEventListener('click', onClick);

对于上面观察者的例子,一旦它们再也不须要(或者关联的对象变成不可达),明确地移除它们很是重要。老的 IE 6 是没法处理循环引用的。由于老版本的 IE 是没法检测 DOM 节点与 JavaScript 代码之间的循环引用,会致使内存泄漏。

可是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经能够正确检测和处理循环引用了。即回收节点内存时,没必要非要调用 removeEventListener 了。

三、脱离 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 元素引用的时候,要当心谨慎。

四、闭包

闭包的关键是匿名函数能够访问父级做用域的变量。

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);

每次调用 replaceThingtheThing 获得一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。someMethod 能够经过 theThing 使用,someMethodunused 分享闭包做用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。

解决方法

replaceThing 的最后添加 originalThing = null

PS:今晚弄到很晚,因为时间问题,就再也不详细介绍Chrome 内存剖析工具,有兴趣的你们去原文查看。

周末汇总将在周日早上发送,周六会发送其余类型的文章,敬请期待。

昨日思考题解答

问题一

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

解答

给一个全局变量赋值为null,至关于将这个变量的指针对象以及值清空,若是是给对象的属性 赋值为null,或者局部变量赋值为null,至关于给这个属性分配了一块空的内存,而后值为null, JS会回收全局变量为null的对象。

给一个全局变量赋值为undefined,至关于将这个对象的值清空,可是这个对象依旧存在,若是是给对象的属性赋值 为undefined,说明这个值为空值

扩展下

声明了一个变量,但未对其初始化时,这个变量的值就是undefined,它是 JavaScript 基本类型 之一。

var data;
console.log(data === undefined); //true

对于还没有声明过的变量,只能执行一项操做,即便用typeof操做符检测其数据类型,使用其余的操做都会报错。

//data变量未定义
console.log(typeof data); // "undefined"
console.log(data === undefined); //报错

null 特指对象的值未设置,它是 JavaScript 基本类型 之一。

null 是一个字面量,它不像undefined 是全局对象的一个属性。null 是表示缺乏的标识,指示变量未指向任何对象。

// foo不存在,它历来没有被定义过或者是初始化过:
foo;
"ReferenceError: foo is not defined"

// foo如今已是知存在的,可是它没有类型或者是值:
var foo = null; 
console.log(foo);    // null

问题二

ES6语法中的 const 声明一个只读的常量,那为何下面能够修改const的值?

const foo = {};

// 为 foo 添加一个属性,能够成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另外一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

解答

const实际上保证的,并非变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,所以等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即老是指向另外一个固定的地址),至于它指向的数据结构是否是可变的,就彻底不能控制了。所以,将一个对象声明为常量必须很是当心。

今日思考题

<script>
    console.log(fun)

    console.log(person)
</script>

<script>
    console.log(person)

    console.log(fun)

    var person = "Eric";

    console.log(person)

    function fun() {
        console.log(person)
        var person = "Tom";
        console.log(person)
    }

    fun()

    console.log(person)
</script>

上面代码的执行结果是什么?先本身分析,而后再到浏览器中执行。

参考

4类 JavaScript 内存泄漏及如何避免

ECMAScript 6 入门之const 命令

往期文章查看

每周计划安排

每周面试重难点计划以下,若有修改会通知你们。每周一期,为期半年,准备明年跳槽的小伙伴们能够把本公众号[置顶]()了。

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 做用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

本人Github连接以下,欢迎各位Star

http://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

若是你想加群讨论每期面试知识点,公众号回复[加群]便可

相关文章
相关标签/搜索