{"id":13508191,"url":"https://github.com/zhansingsong/js-leakage-patterns","last_synced_at":"2026-02-18T22:49:06.423Z","repository":{"id":49804844,"uuid":"112143449","full_name":"zhansingsong/js-leakage-patterns","owner":"zhansingsong","description":":dart:这是关于JavaScript内存泄露和CSS优化相关序列文章，相信你读完会有所收获的:airplane:","archived":false,"fork":false,"pushed_at":"2019-01-15T03:16:54.000Z","size":9644,"stargazers_count":799,"open_issues_count":2,"forks_count":82,"subscribers_count":33,"default_branch":"master","last_synced_at":"2025-02-26T03:35:51.790Z","etag":null,"topics":["chrome-devtools","closure","dom","javascript","memory-leak","memory-leaks","requestanimationframe"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zhansingsong.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-11-27T03:38:50.000Z","updated_at":"2025-01-23T11:03:33.000Z","dependencies_parsed_at":"2022-09-01T13:52:12.034Z","dependency_job_id":null,"html_url":"https://github.com/zhansingsong/js-leakage-patterns","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhansingsong%2Fjs-leakage-patterns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhansingsong%2Fjs-leakage-patterns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhansingsong%2Fjs-leakage-patterns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhansingsong%2Fjs-leakage-patterns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zhansingsong","download_url":"https://codeload.github.com/zhansingsong/js-leakage-patterns/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246307590,"owners_count":20756473,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chrome-devtools","closure","dom","javascript","memory-leak","memory-leaks","requestanimationframe"],"created_at":"2024-08-01T02:00:49.452Z","updated_at":"2026-02-18T22:49:06.356Z","avatar_url":"https://github.com/zhansingsong.png","language":"HTML","funding_links":[],"categories":["HTML"],"sub_categories":[],"readme":"这是关于 JavaScript 内存泄露和 CSS 优化相关序列文章（整理中……）。由于时间有限更新进度会有点慢，但会持续更新的。自己也在学习中，难免对某些知识点的理解不是很正确，所以才将文章放置 github 上，一是想与大家分享，二是方便持续更新，三是便于实时修正错误点。也希望看本文的各位同学能多提 issues，我会根据提的意见不断完善文章。最后希望各位能从文章中有所收获-----\u003e:tada: enjoy reading, enjoy life :whale:\n\n### 序列文章链接\n\n* JavaScript：\n  * [JavaScript 内存那点事](./JavaScript内存那点事/JavaScript内存那点事.md)\n  * [常见的 JavaScript 内存泄露](./常见的JavaScript内存泄露/常见的JavaScript内存泄露.md)\n  * [IE\u003c8 循环引用导致的内存泄露](./IE\u003c8循环引用导致的内存泄露/IE\u003c8循环引用导致的内存泄露.md)\n  * [内存泄露之 jQuery.cache](./内存泄露之jQuery.cache/内存泄露之jQuery.cache.md)\n  * [内存泄露之 Listeners](./内存泄露之Listeners/内存泄露之Listeners.md)\n  * [requestAnimationFrame](./requestAnimationFrame/requestAnimationFrame.md)\n* CSS:\n  * [浏览器渲染简述](./浏览器渲染简述/浏览器渲染简述.md)\n  * [关于外部样式表也许你不知道的事](./关于外部样式表也许你不知道的事/关于外部样式表也许你不知道的事.md)\n* IMAGE\n  * [WebP实战](./WebP实战/WebP实战.md)\n  * [Medium渐进性懒加载](./Medium渐进性懒加载/Medium渐进性懒加载.md)\n\n#### \u003cu style=\"color:#ab2251\"\u003e :no_entry:声明: 本资料仅供学习交流! 如需转载，请注明出处。\u003c/u\u003e\n\n---\n\n# 常见的 JavaScript 内存泄露\n\n![](./images/head.jpg)\n\n## 什么是内存泄露\n\n\u003e **内存泄漏**指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失，而是应用程序分配某段内存后，由于设计错误，导致在释放该段内存之前就失去了对该段内存的控制，从而造成了内存的浪费。内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而，有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏，即使严格意义上来说这是不准确的。\n\u003e ————[wikipedia](https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F)\n\n**⚠️ 注：下文中标注的 CG 是 Chrome 浏览器中 Devtools 的【Collect garbage】按钮缩写，表示回收垃圾操作。**\n![cg](https://raw.githubusercontent.com/zhansingsong/js-leakage-patterns/master/images/CG.png)\n\n## 意外的全局变量\n\nJavaScript 对未声明变量的处理方式：在全局对象上创建该变量的引用(即全局对象上的属性，不是变量，因为它能通过`delete`删除)。如果在浏览器中，全局对象就是**window**对象。\n\n如果未声明的变量缓存大量的数据，会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。\n\n```js\nfunction foo(arg) {\n  bar = 'this is a hidden global variable with a large of data';\n}\n```\n\n等同于：\n\n```js\nfunction foo(arg) {\n  window.bar = 'this is an explicit global variable with a large of data';\n}\n```\n\n另外，通过**this**创建意外的全局变量：\n\n```js\nfunction foo() {\n  this.variable = 'potential accidental global';\n}\n\n// 当在全局作用域中调用foo函数，此时this指向的是全局对象(window)，而不是'undefined'\nfoo();\n```\n\n### 解决方法：\n\n在 JavaScript 文件中添加`'use strict'`，开启严格模式，可以有效地避免上述问题。\n\n```js\nfunction foo(arg) {\n  'use strict'; // 在foo函数作用域内开启严格模式\n  bar = 'this is an explicit global variable with a large of data'; // 报错：因为bar还没有被声明\n}\n```\n\n如果需要在一个函数中使用全局变量，可以像如下代码所示，在**window**上明确声明：\n\n```js\nfunction foo(arg) {\n  window.bar = 'this is a explicit global variable with a large of data';\n}\n```\n\n这样不仅可读性高，而且后期维护也方便\n\n\u003e 谈到全局变量，需要注意那些用来临时存储大量数据的全局变量，确保在处理完这些数据后将其设置为 null 或重新赋值。全局变量也常用来做 cache，一般 cache 都是为了性能优化才用到的，为了性能，最好对 cache 的大小做个上限限制。因为 cache 是不能被回收的，越高 cache 会导致越高的内存消耗。\n\n## console.log\n\n`console.log`：向 web 开发控制台打印一条消息，常用来在开发时调试分析。有时在开发时，需要打印一些对象信息，但发布时却忘记去掉`console.log`语句，这可能造成内存泄露。\n\n在传递给`console.log`的对象是不能被垃圾回收 ♻️，因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中`console.log`任何对象。\n\n### 实例------\u003e[demos/log.html](./demos/log.html)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003eLeaker\u003c/title\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cinput type=\"button\" value=\"click\"\u003e\n  \u003cscript\u003e\n    !function () {\n      function Leaker() {\n        this.init();\n      };\n      Leaker.prototype = {\n        init: function () {\n          this.name = (Array(100000)).join('*');\n          console.log(\"Leaking an object %o: %o\", (new Date()), this);// this对象不能被回收\n        },\n\n        destroy: function () {\n          // do something....\n        }\n      };\n      document.querySelector('input').addEventListener('click', function () {\n        new Leaker();\n      }, false);\n    }()\n  \u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\n这里结合 Chrome 的 Devtools–\u003ePerformance 做一些分析，操作步骤如下：\n\n\u003cu\u003e**:warning:注：最好在隐藏窗口中进行分析工作，避免浏览器插件影响分析结果**\u003c/u\u003e\n\n1.  开启【Performance】项的记录\n2.  执行一次 CG，创建基准参考线\n3.  连续单击【click】按钮三次，新建三个 Leaker 对象\n4.  执行一次 CG\n5.  停止记录\n\n![](./images/console_1.png)\n\n可以看出【JS Heap】线最后没有降回到基准参考线的位置，显然存在没有被回收的内存。如果将代码修改为：\n\n```js\n!(function() {\n  function Leaker() {\n    this.init();\n  }\n  Leaker.prototype = {\n    init: function() {\n      this.name = Array(100000).join('*');\n    },\n\n    destroy: function() {\n      // do something....\n    },\n  };\n  document.querySelector('input').addEventListener(\n    'click',\n    function() {\n      new Leaker();\n    },\n    false,\n  );\n})();\n```\n\n去掉`console.log(\"Leaking an object %o: %o\", (new Date()), this);`语句。重复上述的操作步骤，分析结果如下：\n\n![](./images/console_2.png)\n\n从对比分析结果可知，`console.log`打印的对象是不会被垃圾回收器回收的。因此最好不要在页面中`console.log`任何大对象，这样可能会影响页面的整体性能，特别在生产环境中。除了`console.log`外，另外还有`console.dir`、`console.error`、`console.warn`等都存在类似的问题，这些细节需要特别的关注。\n\n## closures(闭包)\n\n当一个函数 A 返回一个内联函数 B，即使函数 A 执行完，函数 B 也能访问函数 A 作用域内的变量，这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。\n\n```js\nfunction foo(message) {\n  function closure() {\n    console.log(message);\n  }\n  return closure;\n}\n\n// 使用\nvar bar = foo('hello closure!');\nbar(); // 返回 'hello closure!'\n```\n\n在函数 foo 内创建的函数 closure 对象是不能被回收掉的，因为它被全局变量 bar 引用，处于一直可访问状态。通过执行`bar()`可以打印出`hello closure!`。如果想释放掉可以将`bar = null`即可。\n\n\u003cu\u003e**由于闭包会携带包含它的函数的作用域，因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。**\u003c/u\u003e\n\n### 实例------\u003e[demos/closures.html](./demos/closures.html)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003eClosure\u003c/title\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cp\u003e不断单击【click】按钮\u003c/p\u003e\n  \u003cbutton id=\"click_button\"\u003eClick\u003c/button\u003e\n  \u003cscript\u003e\n    function f() {\n      var str = Array(10000).join('#');\n      var foo = {\n        name: 'foo'\n      }\n      function unused() {\n        var message = 'it is only a test message';\n        str = 'unused: ' + str;\n      }\n      function getData() {\n        return 'data';\n      }\n      return getData;\n    }\n\n    var list = [];\n\n    document.querySelector('#click_button').addEventListener('click', function () {\n      list.push(f());\n    }, false);\n  \u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\n这里结合 Chrome 的 Devtools-\u003eMemory 工具进行分析，操作步骤如下：\n\n\u003cu\u003e**:warning:注：最好在隐藏窗口中进行分析工作，避免浏览器插件影响分析结果**\u003c/u\u003e\n\n1.  选中【Record allocation timeline】选项\n2.  执行一次 CG\n3.  单击【start】按钮开始记录堆分析\n4.  连续单击【click】按钮十多次\n5.  停止记录堆分析\n\n![closure](./images/closure1.png)\n\n上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条，过滤出对应新分配的对象：\n\n![closure](./images/closure2.png)\n\n查看对象的详细信息：\n\n![closure](./images/closure3.png)\n\n从图可知，在返回的闭包作用链(Scopes)中携带有它所在函数的作用域，作用域中还包含一个 str 字段。而 str 字段并没有在返回 getData()中使用过。为什么会存在在作用域中，按理应该被 GC 回收掉， why:question:\n\n原因是在相同作用域内创建的多个内部函数对象是共享同一个[变量对象（variable object）](http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/)。如果创建的内部函数没有被其他对象引用，不管内部函数是否引用外部函数的变量和函数，在外部函数执行完，对应变量对象便会被销毁。反之，如果内部函数中存在有对外部函数变量或函数的访问（可以不是被引用的内部函数），并且存在某个或多个内部函数被其他对象引用，那么就会形成闭包，外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因，便可以对症下药了。对代码做如下修改：\n\n```js\nfunction f() {\n  var str = Array(10000).join('#');\n  var foo = {\n    name: 'foo',\n  };\n  function unused() {\n    var message = 'it is only a test message';\n    // str = 'unused: ' + str; //删除该条语句\n  }\n  function getData() {\n    return 'data';\n  }\n  return getData;\n}\n\nvar list = [];\n\ndocument.querySelector('#click_button').addEventListener(\n  'click',\n  function() {\n    list.push(f());\n  },\n  false,\n);\n```\n\ngetData()和 unused()内部函数共享 f 函数对应的变量对象，因为 unused()内部函数访问了 f 作用域内 str 变量，所以 str 字段存在于 f 变量对象中。加上 getData()内部函数被返回，被其他对象引用，形成了闭包，因此对应的 f 变量对象存在于闭包函数的作用域链中。这里只要将函数 unused 中`str = 'unused: ' + str;`语句删除便可解决问题。\n\n![closure](./images/closure4.png)\n\n查看一下闭包信息：\n\n![closure](./images/closure5.png)\n\n## DOM 泄露\n\n在 JavaScript 中，DOM 操作是非常耗时的。因为 JavaScript/ECMAScript 引擎独立于渲染引擎，而 DOM 是位于渲染引擎，相互访问需要消耗一定的资源。如 Chrome 浏览器中 DOM 位于 WebCore，而 JavaScript/ECMAScript 位于 V8 中。假如将 JavaScript/ECMAScript、DOM 分别想象成两座孤岛，两岛之间通过一座收费桥连接，过桥需要交纳一定“过桥费”。JavaScript/ECMAScript 每次访问 DOM 时，都需要交纳“过桥费”。因此访问 DOM 次数越多，费用越高，页面性能就会受到很大影响。[了解更多:information_source:](http://www.phpied.com/dom-access-optimization/)\n\n![](http://www.phpied.com/wp-content/uploads/2009/12/domlandia.png)\n\n为了减少 DOM 访问次数，一般情况下，当需要多次访问同一个 DOM 方法或属性时，会将 DOM 引用缓存到一个局部变量中。\u003cu\u003e但如果在执行某些删除、更新操作后，可能会忘记释放掉代码中对应的 DOM 引用，这样会造成 DOM 内存泄露。\u003c/u\u003e\n\n### 实例------\u003e[demos/dom.html](./demos/dom.html)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003eDom-Leakage\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cinput type=\"button\" value=\"remove\" class=\"remove\" style=\"display:none;\"\u003e\n  \u003cinput type=\"button\" value=\"add\" class=\"add\"\u003e\n\n  \u003cdiv class=\"container\"\u003e\n    \u003cpre class=\"wrapper\"\u003e\u003c/pre\u003e\n  \u003c/div\u003e\n  \u003cscript\u003e\n    // 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点，将其缓存到本地变量中，\n    var wrapper = document.querySelector('.wrapper');\n    var container = document.querySelector('.container');\n    var removeBtn = document.querySelector('.remove');\n    var addBtn = document.querySelector('.add');\n    var counter = 0;\n    var once = true;\n    // 方法\n    var hide = function(target){\n      target.style.display = 'none';\n    }\n    var show = function(target){\n      target.style.display = 'inline-block';\n    }\n    // 回调函数\n    var removeCallback = function(){\n      removeBtn.removeEventListener('click', removeCallback, false);\n      addBtn.removeEventListener('click', addCallback, false);\n      hide(addBtn);\n      hide(removeBtn);\n      container.removeChild(wrapper);\n    }\n    var addCallback = function(){\n      wrapper.appendChild(document.createTextNode('\\t' + ++counter + '：a new line text\\n'));\n      // 显示删除操作按钮\n      if(once){\n        show(removeBtn);\n        once = false;\n      }\n    }\n    // 绑定事件\n    removeBtn.addEventListener('click', removeCallback, false);\n    addBtn.addEventListener('click', addCallback, false);\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n这里结合 Chrome 浏览器的 Devtools–\u003ePerformance 做一些分析，操作步骤如下：\n\n\u003cu\u003e**:warning:注：最好在隐藏窗口中进行分析工作，避免浏览器插件影响分析结果**\u003c/u\u003e\n\n1.  开启【Performance】项的记录\n2.  执行一次 CG，创建基准参考线\n3.  连续单击【add】按钮 6 次，增加 6 个文本节点到 pre 元素中\n4.  单击【remove】按钮，删除刚增加 6 个文本节点和 pre 元元素\n5.  执行一次 CG\n6.  停止记录堆分析\n\n![dom](./images/dom1.png)\n\n从分析结果图可知，虽然 6 次 add 操作增加 6 个 Node，但是 remove 操作并没有让 Nodes 节点数下降，即 remove 操作失败。尽管还主动执行了一次 CG 操作，Nodes 曲线也没有下降。因此可以断定内存泄露了！那问题来了，如何去查找问题的原因呢？这里可以通过 Chrome 浏览器的 Devtools–\u003eMemory 进行诊断分析，执行如下操作步骤：\n\n\u003cu\u003e**:warning:注：最好在隐藏窗口中进行分析工作，避免浏览器插件影响分析结果**\u003c/u\u003e\n\n1.  选中【Take heap snapshot】选项\n2.  连续单击【add】按钮 6 次，增加 6 个文本节点到 pre 元素中\n3.  单击【Take snapshot】按钮，执行一次堆快照\n4.  单击【remove】按钮，删除刚增加 6 个文本节点和 pre 元元素\n5.  单击【Take snapshot】按钮，执行一次堆快照\n6.  选中生成的第二个快照报告，并将视图由\"Summary\"切换到\"Comparison\"对比模式，在[class filter]过滤输入框中输入关键字：**Detached**\n\n![dom](./images/dom2.png)\n\n从分析结果图可知，导致整个 pre 元素和 6 个文本节点无法别回收的原因是：代码中存在全局变量`wrapper`对 pre 元素的引用。知道了产生的问题原因，便可对症下药了。对代码做如下就修改：\n\n```js\n// 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点，将其缓存到本地变量中，\nvar wrapper = document.querySelector('.wrapper');\nvar container = document.querySelector('.container');\nvar removeBtn = document.querySelector('.remove');\nvar addBtn = document.querySelector('.add');\nvar counter = 0;\nvar once = true;\n// 方法\nvar hide = function(target) {\n  target.style.display = 'none';\n};\nvar show = function(target) {\n  target.style.display = 'inline-block';\n};\n// 回调函数\nvar removeCallback = function() {\n  removeBtn.removeEventListener('click', removeCallback, false);\n  addBtn.removeEventListener('click', addCallback, false);\n  hide(addBtn);\n  hide(removeBtn);\n  container.removeChild(wrapper);\n\n  wrapper = null; //在执行删除操作时，将wrapper对pre节点的引用释放掉\n};\nvar addCallback = function() {\n  wrapper.appendChild(\n    document.createTextNode('\\t' + ++counter + '：a new line text\\n'),\n  );\n  // 显示删除操作按钮\n  if (once) {\n    show(removeBtn);\n    once = false;\n  }\n};\n// 绑定事件\nremoveBtn.addEventListener('click', removeCallback, false);\naddBtn.addEventListener('click', addCallback, false);\n```\n\n在执行删除操作时，将 wrapper 对 pre 节点的引用释放掉，即在删除逻辑中增加`wrapper = null;`语句。再次在 Devtools–\u003ePerformance 中重复上述操作：\n\n![dom](./images/dom3.png)\n\n### 小试牛刀------\u003e[demos/dom_practice.html](./demos/dom_practice.html)\n\n再来看看网上的一个实例，代码如下：\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003ePractice\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv id=\"refA\"\u003e\u003cul\u003e\u003cli\u003e\u003ca href=\"#\"\u003e\u003c/a\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#\"\u003e\u003c/a\u003e\u003c/li\u003e\u003cli\u003e\u003ca href=\"#\" id=\"refB\"\u003e\u003c/a\u003e\u003c/li\u003e\u003c/ul\u003e\u003c/div\u003e\n  \u003cdiv\u003e\u003c/div\u003e\n  \u003cdiv\u003e\u003c/div\u003e\n\n  \u003cscript\u003e\n    var refA = document.getElementById('refA');\n    var refB = document.getElementById('refB');\n    document.body.removeChild(refA);\n\n    // #refA不能GC回收，因为存在变量refA对它的引用。将其对#refA引用释放，但还是无法回收#refA。\n    refA = null;\n\n    // 还存在变量refB对#refA的间接引用(refB引用了#refB，而#refB属于#refA)。将变量refB对#refB的引用释放，#refA就可以被GC回收。\n    refB = null;\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n整个过程如下图所演示：\n\n![](./images/memory.gif)\n\n有兴趣的同学可以使用 Chrome 的 Devtools 工具，验证一下分析结果，实践很重要~~~:high_brightness:\n\n## timers\n\n在 JavaScript 常用`setInterval()`来实现一些动画效果。当然也可以使用链式`setTimeout()`调用模式来实现：\n\n```js\nsetTimeout(function() {\n  // do something. . . .\n  setTimeout(arguments.callee, interval);\n}, interval);\n```\n\n如果在不需要`setInterval()`时，没有通过`clearInterval()`方法移除，那么`setInterval()`会不停地调用函数，直到调用`clearInterval()`或窗口关闭。如果链式`setTimeout()`调用模式没有给出终止逻辑，也会一直运行下去。因此再不需要重复定时器时，确保对定时器进行清除，避免占用系统资源。另外，在使用`setInterval()`和`setTimeout()`来实现动画时，无法确保定时器按照指定的时间间隔来执行动画。为了能在 JavaScript 中创建出平滑流畅的动画，浏览器为 JavaScript 动画添加了一个新 API-requestAnimationFrame()。[关于 setInterval、setTimeout 与 requestAnimationFrame 实现动画上的区别 ➹ 猛击 😊](https://github.com/zhansingsong/js-leakage-patterns/blob/master/requestAnimationFrame/requestAnimationFrame.md)\n\n### 实例------\u003e[demos/timers.html](./demos/timers.html)\n\n如下通过`setInterval()`实现一个 clock 的小实例，不过代码存在问题的，有兴趣的同学可以先尝试找一下问题的所在~~~~~😎\n操作：\n\n* 单击【start】按钮开始 clock，同时 web 开发控制台会打印实时信息\n* 单击【stop】按钮停止 clock，同时 web 开发控制台会输出停止信息\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003esetInterval\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cinput type=\"button\" value=\"start\" class=\"start\"\u003e\n  \u003cinput type=\"button\" value=\"stop\" class=\"stop\"\u003e\n\n  \u003cscript\u003e\n    var counter = 0;\n    var clock = {\n      start: function () {\n        setInterval(this.step.bind(null, ++counter), 1000);\n      },\n      step: function (flag) {\n        var date = new Date();\n        var h = date.getHours();\n        var m = date.getMinutes();\n        var s = date.getSeconds();\n        console.log(\"%d-----\u003e %d:%d:%d\", flag, h, m, s);\n      }\n    }\n    document.querySelector('.start').addEventListener('click', clock.start.bind(clock), false);\n    document.querySelector('.stop').addEventListener('click', function () {\n      console.log('----\u003e stop \u003c----');\n      clock = null;\n    }, false);\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n上述代码存在两个问题：\n\n1.  如果不断的单击【start】按钮，会断生成新的 clock。\n\n2.  单击【stop】按钮不能停止 clock。\n\n输出结果:\n\n![](./images/setinterval.png)\n\n针对暴露出的问题，对代码做如下修改：\n\n```js\nvar counter = 0;\nvar clock = {\n  timer: null,\n  start: function() {\n    // 解决第一个问题\n    if (this.timer) {\n      clearInterval(this.timer);\n    }\n    this.timer = setInterval(this.step.bind(null, ++counter), 1000);\n  },\n  step: function(flag) {\n    var date = new Date();\n    var h = date.getHours();\n    var m = date.getMinutes();\n    var s = date.getSeconds();\n    console.log('%d-----\u003e %d:%d:%d', flag, h, m, s);\n  },\n  // 解决第二个问题\n  destroy: function() {\n    console.log('----\u003e stop \u003c----');\n    clearInterval(this.timer);\n    node = null;\n    counter = void 0;\n  },\n};\ndocument\n  .querySelector('.start')\n  .addEventListener('click', clock.start.bind(clock), false);\ndocument\n  .querySelector('.stop')\n  .addEventListener('click', clock.destroy.bind(clock), false);\n```\n\n## EventListener\n\n做移动开发时，需要对不同设备尺寸做适配。如在开发组件时，有时需要考虑处理横竖屏适配问题。一般做法，在横竖屏发生变化时，需要将组件销毁后再重新生成。而在组件中会对其进行相关事件绑定，如果在销毁组件时，没有将组件的事件解绑，在横竖屏发生变化时，就会不断地对组件进行事件绑定。这样会导致一些异常，甚至可能会导致页面崩掉。\n\n### 实例------\u003e[demos/callbacks.html](./demos/callbacks.html)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n  \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"\u003e\n  \u003ctitle\u003ecallbacks\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv class=\"container\"\u003e\u003c/div\u003e\n  \u003cscript\u003e\n    var container = document.querySelector('.container');\n    var counter = 0;\n    var createHtml = function (n, counter) {\n      var template = `${(new Array(n)).join(`\u003cdiv\u003e${counter}: this is a new data \u003cinput type=\"button\" value=\"remove\"\u003e\u003c/div\u003e`)}`\n      container.innerHTML = template;\n    }\n\n    var resizeCallback = function (init) {\n      createHtml(10, ++counter);\n      // 事件委托\n      container.addEventListener('click', function (event){\n        var target = event.target;\n          if(target.tagName === 'INPUT'){\n              container.removeChild(target.parentElement)\n          }\n      }, false);\n    }\n    window.addEventListener('resize', resizeCallback, false);\n    resizeCallback(true);\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n页面是存在问题的，这里结合 Devtools–\u003ePerformance 分析一下问题所在，操作步骤如下：\n\n\u003cu\u003e**:warning:注：最好在隐藏窗口中进行分析工作，避免浏览器插件影响分析结果**\u003c/u\u003e\n\n1.  开启 Performance 项的记录\n2.  执行一次 CG，创建基准参考线\n3.  对窗口大小进行调整\n4.  执行一次 CG\n5.  停止记录\n\n![callbacks](./images/callback.png)\n\n如分析结果所示，在窗口大小变化时，会不断地对`container`添加代理事件。\n\n同一个元素节点注册了多个相同的 EventListener，那么重复的实例会被抛弃。这么做不会让得 EventListener 被重复调用，也不需要用 removeEventListener 手动清除多余的 EventListener，因为重复的都被自动抛弃了。而这条规则只是针对于命名函数。[对于匿名函数，浏览器会将其看做不同的 EventListener](https://triangle717.wordpress.com/2015/12/14/js-avoid-duplicate-listeners/)，所以只要将匿名的 EventListener，命名一下就可以解决问题：\n\n```js\nvar container = document.querySelector('.container');\nvar counter = 0;\nvar createHtml = function(n, counter) {\n  var template = `${new Array(n).join(\n    `\u003cdiv\u003e${counter}: this is a new data \u003cinput type=\"button\" value=\"remove\"\u003e\u003c/div\u003e`,\n  )}`;\n  container.innerHTML = template;\n};\n//\nvar clickCallback = function(event) {\n  var target = event.target;\n  if (target.tagName === 'INPUT') {\n    container.removeChild(target.parentElement);\n  }\n};\nvar resizeCallback = function(init) {\n  createHtml(10, ++counter);\n  // 事件委托\n  container.addEventListener('click', clickCallback, false);\n};\nwindow.addEventListener('resize', resizeCallback, false);\nresizeCallback(true);\n```\n\n在 Devtools–\u003ePerformance 中再重复上述操作，分析结果如下：\n![callback](./images/callback1.png)\n\n在开发中，开发者很少关注事件解绑，因为浏览器已经为我们处理得很好了。不过在使用第三方库时，需要特别注意，因为一般第三方库都实现了自己的事件绑定，如果在使用过程中，在需要销毁事件绑定时，没有调用所解绑方法，就可能造成事件绑定数量的不断增加。如下链接是我在项目中使用 jquery，遇见到类似问题：[jQuery 中忘记解绑注册的事件，造成内存泄露 ➹ 猛击 😊](https://github.com/zhansingsong/js-leakage-patterns/blob/master/%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E4%B9%8BListeners/%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E4%B9%8BListeners.md)\n\n## 总结\n\n本文主要介绍了几种常见的内存泄露。在开发过程，需要我们特别留意一下本文所涉及到的几种内存泄露问题。因为这些随时可能发生在我们日常开发中，如果我们对它们不了解是很难发现它们的存在。可能在它们将问题影响程度放大时，才会引起我们的关注。不过那时可能就晚了，因为产品可能已经上线，接着就会严重影响产品的质量和用户体验，甚至可能让我们承受大量用户流失的损失。作为开发的我们必须把好这个关，让我们开发的产品带给用户最好的体验。\n\n## 参考文章：\n\n* [An interesting kind of JavaScript memory leak](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156)\n* [Memory Leaks in Microsoft Internet Explorer](http://isaacschlueter.com/2006/10/msie-memory-leaks/trackback/index.html)\n* [Memory leak when logging complex objects](https://stackoverflow.com/questions/12996129/memory-leak-when-logging-complex-objects)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhansingsong%2Fjs-leakage-patterns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzhansingsong%2Fjs-leakage-patterns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhansingsong%2Fjs-leakage-patterns/lists"}