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

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

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

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

如果觉得本系列不错,欢迎转发,您的支持就是我坚持的最大动力。

本期推荐文章

4类 JavaScript 内存泄漏及如何避免 ,由于微信不能访问外链,点击阅读原文就可以啦。

推荐理由

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

阅读笔记

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

垃圾回收算法

常用垃圾回收算法叫做**标记清除 (Mark-and-sweep) **,算法由以下几步组成:

  • 1、垃圾回收器创建了一个“roots”列表。roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);

  • 2、所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾。

  • 3、所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。

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

四种常见的JS内存泄漏

划重点 这是个考点

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

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

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

解决方法

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

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

计时器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 了。

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

每次调用 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

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

如果你想加群讨论每期面试知识点,公众号回复[加群]即可

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容