javascript 递归setTimeout递归会造成内存泄漏吗

javascript setTimeout递归会造成内存泄漏吗? - 知乎8被浏览2770分享邀请回答var call = function(x) {
console.log(x++);
setTimeout(function() { call(x); }, 1);
# 更新刚才看到一篇文章 ,我于是试了一下:似乎 setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。我自己实验了一下,似乎确实如此。不过就算不考虑这个理由,习惯良好的人也从不会用字符串作为 setTimeout 的第一个参数的……2添加评论分享收藏感谢收起var call = function(x) {
console.log(x++);
setTimeout(function() {
以这段代码为例,的确x这个变量会一直存在在内存之中,但这么一个变量还不至于让内存达到溢出。并且可以在chrome Dev Tool中模拟一次垃圾回收结果:&img src="" data-rawwidth="1366" data-rawheight="736" class="content_image" width="1366" data-original=""&可以看到js heap一直在增加可以看到js heap一直在增加&img src="" data-rawwidth="1366" data-rawheight="173" class="content_image" width="1366" data-original=""&但是console 依然在跑,其实这已经算是内存泄露了。但是console 依然在跑,其实这已经算是内存泄露了。如果内存会周期性地按时增长,泄露最终将导致浏览器变慢或者停止执行脚本。从性能优化角度来讲已经是不得不做的事情了。试想如果循环执行的代码中包含了一个大数组new Array(1000000).join('*'),那是非常危险的。一般setTimeout递归是在做轮询,这会需要给一个结束条件,执行clearTimeout。1添加评论分享收藏感谢收起JavaScript闭包会造成内存泄漏吗?- 百度派
{{ mainData.uname }}
:{{ mainData.content }}
{{ prevData.uname }}
:{{ prevData.content }}
{{ mainData.uname }}
:{{ mainData.content }}
0 || contentType !== 1" class="separate-line">
:{{ item.content }}
Javascript
JavaScript
获取百度派权限,参与更多问答互动
3">等{{ uList.length }}人邀请你回答
JavaScript闭包会造成内存泄漏吗?
问题说明(可选):
扫一扫分享到微信
{{ log.sign }}
可能对你的问题感兴趣
暂无相关用户
,才能邀请用户
,才能邀请用户
你已邀请15人,不可再邀请
,一个一个又一个。。。
先讲讲什么闭包吧,讲完或许题主自己就有答案了。闭包并不是JS的特色功能, 追究起来,它是一个计算机基础概念,如果想深入理解还真要费那么点工夫.在理解闭包之前, 首先要清楚JS中的作用域只有2种: 全局作...
先讲讲什么闭包吧,讲完或许题主自己就有答案了。闭包并不是JS的特色功能, 追究起来,它是一个计算机基础概念,如果想深入理解还真要费那么点工夫.在理解闭包之前, 首先要清楚JS中的作用域只有2种: 全局作用域和方法作用域全局作用域很好理解了, 方法作用域就是指一个 function 形成一个独立的作用域, 而且方法作用域还能够嵌套.与别的语言不同的是: 花括号({})不能形成一个独立的作用域, 例如Java中的作用域.下面我们举例说说作用域var g = 0;function f() { & &// 这里面就形成了一个方法作用域, 能够保护其中的变量不能被外部访问 & &// 方法作用域能够访问全局作用域 & &var a = 1; & &console.log(g); & &// 嵌套方法作用域 & &function ff() { & & & &// 这里面再度形成了一个方法作用域 & & & &// 其中可以访问外部的那个方法作用域 & & & &var aa = 2; & & & &console.log(a); & &} & &// 出了 ff 的作用域就不能访问其中的东西了 & &// console.log(aa); // 报错 ReferenceError: aa is not defined}f();// console.log(a); // 报错 ReferenceError: a is not defined弄清楚作用域的问题后, 我们就可以开始聊聊闭包了.我们以最经典的for循环为例来讲解. 大家可以试试下面这段代码, 取自JavaScript 秘密花园循环中的闭包for(var i = 0; i & 10; i++) { & &setTimeout(function() { & & & &console.log(i); & &}, 1000);}如果在运行前, 你没有猜到正确答案, 那就对了...1. 首先说说为什么最终输出的是10次10, 而不是你想象中的 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 & 因为setTimeout是异步的! & 你可以想象由于setTimeout是异步的, 因此我们将这个for循环拆成2个部分 & 第一个部分专门处理 i 值的变化, 第二个部分专门来做setTimeout & 因此我们可以得到如下代码 & // 第一个部分 & i++; & ...
& i++; // 总共做10次 & // 第二个部分 & setTimeout(function() { & & &console.log(i); & }, 1000); & ... & setTimeout(function() { & & &console.log(i); & }, 1000); // 总共做10次 & 这样一拆后, 我相信你肯定知道之前那个for循环的运行结果了. & 由于循环中的变量 i 一直在变, 最终会变成10, 而循环每每执行setTimeout时, 其中的方法还没有真正运行, 等真正到时间执行时, i 的值已经变成 10 了! & i 变化的整个过程是瞬间完成的, 总之比你异步要快, 就算你setTimout是0毫秒也一样, 会先于你执行完成. & for(var i = 0; i & 10; i++) { & & & setTimeout(function() { & & & & & console.log(i); & & & }, 0); & }2. 那么为什么setTimeout中匿名function没有形成闭包呢? & 因为setTimeout中的匿名function没有将 i 作为参数传入来固定这个变量的值, 让其保留下来, 而是直接引用了外部作用域中的 i, 因此 i 变化时, 也影响到了匿名function. & 因此如果我们定义一个外部函数, 让 i 作为参数传入即可&闭包&我们要的变量了!! & for (var i = 0; i & 10; i++) { & & & // 注意关键是我们把想要闭包的值当参数传入一个方法 & & & // 这个方法 return 一个新的方法 -- 闭包!! & & & setTimeout(fn(i), 1000); & } & function fn() { // 为了深刻理解闭包, 这个函数我没有用参数 & & & // 神奇的&闭包&发生在这一步, 其实就是作用域和值复制在起了关键作用, & & & // 对于数字/字符等类型是复制值, 而不是引用 & & & var a = arguments[0]; & & & return function() { & & & & & console.log(a); // 注意现在我操作的变量已经变成 a 了, & & & & & & & & & & & & & & & & & & // 已经和 i 没有半毛线关系了! & & & & & & & & & & & & & & & & & & // 而 a 的值就是当时执行时赋予的一个确定值, & & & & & & & & & & & & & & & & & & // 不会因 i 的变化而变化了! & & & }; & }3. 再换成更简洁的方式看你能不能真正理解闭包 & for (var i = 0; i & 10; i++) { & & & (function(a) { & & & & & // 变量 i 的值在传递到这个作用域时被复制给了 a, & & & & & // 因此这个值就不会随外部变量而变化了 & & & & & setTimeout(function() { & & & & & & & console.log(a); & & & & & }, 1000); & & & })(i); // 我们在这里传入参数来&闭包&变量 & }这就是我所理解的闭包, 简单点说就是专门用来&包养&变量的.我就是上面的那个变量 i, 求&包养&真正理解了作用域也就理解了闭包.所以,你觉得会泄露吗?
扫一扫分享到微信
JavaScript闭包会造成内存泄漏吗?
,才能进行回答
一个问题只能回答一次,请&nbsp点击此处&nbsp查看你的答案
5人关注了此问题在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
(function repaint(){
//重绘相关代码
setTimeout(repaint,16) ;
(function repaint(){
//重绘相关代码
requestAnimationFrame(repaint) ;
我在函数内部加了断点,在chrome里面查看call stack,始终是一层,这是否能证明一个函数,在setTimeout和requestAnimationFrame内调用自己不造成递归?
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
答案是不会。
首先我们来理解一下什么叫递归,先不贴wiki上的说明了,大家普遍认识是,递归就是函数自身调用自身。这里面很重要的一点就是,在调用自身的过程中,父函数是没有退出的。需要等被调用的子函数退出后父函数才退出。
但是setTimeout函数不是这样执行的,它只是创建了一个定时任务,然后直接退出了,javascript的引擎线程会将这个任务放到自己的定时任务中,当定时结束后才调用子函数。
可以参考下图(来源于网络)
详细可以参考:
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
不会。因为当前函数执行完毕才会去处理超时处理函数。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
这个分逻辑上和实现上。
逻辑上来说,它和递归是等价的。
但是实现上,它是异步执行的,不会导致栈在使用上的无限增加,因为之前的函数执行完就销毁了。
lz可以这么认为,这种异步形式的递归,不会导致栈空间的溢出。但是作为副作用,这种递归不能依靠异步调用的返回值执行操作(当然,后面可以有操作,但是无法利用到返回值)。和尾递归很像呢,至少在逻辑上两者也确实非常接近。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
setTimeout是“异步”执行的(在我看来其实是伪异步,JS是单线程的,所谓的异步只是通过适当地插入到代码执行队列中延迟执行而已),其实总体相当于在一段时间内定期执行一次repaint函数而已。只不过第一次是立刻执行罢了。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
可以叫循环,但不能叫递归
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
虽然称不上递归,但还是感觉repaint这个方法会被调用多次的。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
每隔一段时间生产一个settimeout对象也会有开销的吧
同步到新浪微博
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:JavaScript(8)
本文将探索常见的客户端 JavaScript 内存泄漏,以及如何使用 Chrome 开发工具发现问题。
内存泄漏是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。
什么是内存泄漏?
本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。
JavaScript 内存管理
JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。
JavaScript 内存泄漏
垃圾回收语言的内存泄漏主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。
Mark-and-sweep
大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。
不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。
为了理解 JavaScript 中最常见的内存泄漏,我们需要了解哪种方式的引用容易被遗忘。
三种类型的常见 JavaScript 内存泄漏
1:意外的全局变量
JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。
JavaScript
123function foo(arg) {&&&&bar = &this is a hidden global variable&;}真相是:JavaScript
functionfoo(arg){
&&&&window.bar=&this
is an explicit global variable&;
函数 foo 内部忘记使用 var ,意外创建了一个全局变量。此例泄漏了一个简单的字符串,无伤大雅,但是有更糟的情况。
另一种意外的全局变量可能由 this 创建:
JavaScript
1234567function foo() {&&&&this.variable = &potential accidental global&;}&// Foo 调用自己,this 指向了全局对象(window)// 而不是 undefinedfoo();在 JavaScript 文件头部加上 ‘use strict’,可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。全局变量注意事项尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。2:被遗忘的计时器或回调函数在 JavaScript 中使用 setInterval 非常平常。一段常见的代码:JavaScript
varsomeResource=getData();
setInterval(function(){
&&&&varnode=document.getElementById('Node');
&&&&if(node){
&&&&&&&&//
处理 node 和 someResource
&&&&&&&&node.innerHTML=JSON.stringify(someResource));
此例说明了什么:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。
对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。
观察者代码示例:
JavaScript
123456var element = document.getElementById('button');function onClick(event) {&&&&element.innerHTML = 'text';}&element.addEventListener('click', onClick);对象观察者和循环引用注意事项老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener 了。3:脱离 DOM 的引用有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。JavaScript
varelements={
&&&&button:document.getElementById('button'),
&&&&image:document.getElementById('image'),
&&&&text:document.getElementById('text')
functiondoStuff(){
&&&&image.src='http://some.url/image';
&&&&button.click();
&&&&console.log(text.innerHTML);
functionremoveButton(){
按钮是 body 的后代元素
&&&&document.body.removeChild(document.getElementById('button'));
此时,仍旧存在一个全局的 #button 的引用
elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 &td& 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 &td& 以外的其它节点。实际情况并非如此:此&td& 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 &td& 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。
闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。
代码示例:
JavaScript
1234567891011121314151617var 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 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。&解释了如何修复此种问题。在 replaceThing 的最后添加 originalThing = null 。Chrome 内存剖析工具概览Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具。与内存相关的两个重要的工具:timeline 和 profiles。timeline 可以检测代码中不需要的内存。在此截图中,我们可以看到潜在的泄漏对象稳定的增长,数据采集快结束时,内存占用明显高于采集初期,Node(节点)的总量也很高。种种迹象表明,代码中存在 DOM 节点泄漏的情况。ProfilesProfiles 是你可以花费大量时间关注的工具,它可以保存快照,对比 JavaScript 代码内存使用的不同快照,也可以记录时间分配。每一次结果包含不同类型的列表,与内存泄漏相关的有 summary(概要) 列表和 comparison(对照) 列表。summary(概要) 列表展示了不同类型对象的分配及合计大小:shallow size(特定类型的所有对象的总大小),retained size(shallow size 加上其它与此关联的对象大小)。它还提供了一个概念,一个对象与关联的 GC root 的距离。对比不同的快照的 comparison list 可以发现内存泄漏。实例:使用 Chrome 发现内存泄漏实质上有两种类型的泄漏:周期性的内存增长导致的泄漏,以及偶现的内存泄漏。显而易见,周期性的内存泄漏很容易发现;偶现的泄漏比较棘手,一般容易被忽视,偶尔发生一次可能被认为是优化问题,周期性发生的则被认为是必须解决的 bug。以&中的代码为例:JavaScript
functioncreateSomeNodes(){
&&&&vardiv,
&&&&&&&&i=100,
&&&&&&&&frag=document.createDocumentFragment();
&&&&for(;i&0;i--){
&&&&&&&&div=document.createElement(&div&);
&&&&&&&&div.appendChild(document.createTextNode(i+&
- &+newDate().toTimeString()));
&&&&&&&&frag.appendChild(div);
&&&&document.getElementById(&nodes&).appendChild(frag);
functiongrow(){
&&&&x.push(newArray(1000000).join('x'));
&&&&createSomeNodes();
&&&&setTimeout(grow,1000);
当 grow 执行的时候,开始创建 div 节点并插入到 DOM 中,并且给全局变量分配一个巨大的数组。通过以上提到的工具可以检测到内存稳定上升。
找出周期性增长的内存
timeline 标签擅长做这些。在 Chrome 中,打开
Dev Tools ,切换到 timeline,勾选 memory 并点击记录按钮,然后点击页面上的 The Button 按钮。过一阵停止记录看结果:
两种迹象显示出现了内存泄漏,图中的 Nodes(绿线)和 JS heap(蓝线)。Nodes 稳定增长,并未下降,这是个显著的信号。
JS heap 的内存占用也是稳定增长。由于垃圾收集器的影响,并不那么容易发现。图中显示内存占用忽涨忽跌,实际上每一次下跌之后,JS heap 的大小都比原先大了。换言之,尽管垃圾收集器不断的收集内存,内存还是周期性的泄漏了。
确定存在内存泄漏之后,我们找找根源所在。
保存两个快照
切换到 Chrome Dev Tools 的 profiles 标签,刷新页面,等页面刷新完成之后,点击 Take Heap Snapshot 保存快照作为基准。而后再次点击 The Button 按钮,等数秒以后,保存第二个快照。
筛选菜单选择 Summary,右侧选择 Objects allocated between Snapshot 1 and Snapshot 2,或者筛选菜单选择 Comparison ,然后可以看到一个对比列表。
此例很容易找到内存泄漏,看下 (string) 的 Size Delta Constructor,8MB,58个新对象。新对象被分配,但是没有释放,占用了8MB。
如果展开 (string) Constructor,会看到许多单独的内存分配。选择某一个单独的分配,下面的 retainers 会吸引我们的注意。
我们已选择的分配是数组的一部分,数组关联到 window 对象的 x 变量。这里展示了从巨大对象到无法回收的 root(window)的完整路径。我们已经找到了潜在的泄漏以及它的出处。
我们的例子还算简单,只泄漏了少量的 DOM 节点,利用以上提到的快照很容易发现。对于更大型的网站,Chrome 还提供了 Record Heap Allocations 功能。
Record heap allocations 找内存泄漏
回到 Chrome Dev Tools 的 profiles 标签,点击 Record Heap Allocations。工具运行的时候,注意顶部的蓝条,代表了内存分配,每一秒有大量的内存分配。运行几秒以后停止。
上图中可以看到工具的杀手锏:选择某一条时间线,可以看到这个时间段的内存分配情况。尽可能选择接近峰值的时间线,下面的列表仅显示了三种 constructor:其一是泄漏最严重的(string),下一个是关联的 DOM 分配,最后一个是 Text constructor(DOM 叶子节点包含的文本)。
从列表中选择一个 HTMLDivElement constructor,然后选择 Allocation stack。
现在知道元素被分配到哪里了吧(grow -& createSomeNodes),仔细观察一下图中的时间线,发现 HTMLDivElement constructor 调用了许多次,意味着内存一直被占用,无法被 GC 回收,我们知道了这些对象被分配的确切位置(createSomeNodes)。回到代码本身,探讨下如何修复内存泄漏吧。
另一个有用的特性
在 heap allocations 的结果区域,选择 Allocation。
这个视图呈现了内存分配相关的功能列表,我们立刻看到了 grow 和 createSomeNodes。当选择 grow 时,看看相关的 object constructor,清楚地看到 (string), HTMLDivElement 和 Text 泄漏了。
结合以上提到的工具,可以轻松找到内存泄漏。
原文地址:/86244/
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:979次
排名:千里之外
原创:13篇
(3)(2)(4)(3)(2)(1)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'

我要回帖

更多关于 javascript 尾递归 的文章

 

随机推荐