小弟也是初学JS,请教一下各位大牛!
代码片段:
(function() {
var li = document.getElementById('nav').getElementsByTagName('li'),
i = 0;
for (; i < li.length; i++) {
li[i].index = i;
li[i].onmouseover = function() {
for (var j = 0; j < li.length; j++) {
li[j].getElementsByTagName('a')[0].className = '';
}
li[this.index].getElementsByTagName('a')[0].className = 'current';
}
}
}());
我就是想问下这里的 li 变量,当函数执行完成时,有没有得到内存的释放?
我个人认为觉得并没有得到释放,因为 onmouseouver 事件,闭包引用了 li 外面作用域的变量,所以垃圾回收器,标记一直是1,所以没有得到释放。
如果要想释放内存,以上代码应该怎样改呢?
谢邀。
首先我要说一句题主可能不爱听的话,但是我不得不说:我讨厌这种问题。我讨厌这种问题的原因有很多,但是为了不跑题我只说一点,最最实际的一点,就是成天琢磨这种问题事倍功半,得不偿失。
就题目给出的代码,根本不会造成内存泄露(下面我会证明),也没有必要释放内存,因为你的事件就绑定在
li
们的身上,你代码的意图又是一定要利用这些事件的回调函数,除非你确定在某一时刻之后不再需要这些事件监听了,否则你把li
对象释放掉(回收)有什么意义呢?你干脆别写这些代码不就好了吗?我们在看一些介绍内存泄露的文章时,经常会读到类似的示例代码,但那是纯演示性质的,而不是告诉你:“在实际中这是错的,你不应该这样做!”真要是这样,我们啥都别干了……(当然,有一些最佳实践是公认的,比如
null
化不需要的对象引用)。我们应该明白“内存泄露”的本质是什么。以 V8 为例,GC(垃圾回收)的工作需要建立在跟踪“活着的对象”的基础上,所谓“活着”就是指现在占用了内存资源的,并且是我们真的在用的,比如本题中的
li
们。对应的还有一种叫做“死掉的对象”,是指已经被回收的,或者说正确的、恰当的被释放的那些对象们——这里有一个很重要的前提,就是这些对象是真的没用了!不管是在代码逻辑里没用了,还是在业务逻辑没用了,我们回收它们是因为没用了才回收,而不是因为它们占用内存而回收!!!这就是我讨厌这个问题的本质原因,明明就是要绑定事件在这些
li
身上(代码逻辑),而且之后也肯定要一直保持它有效(业务逻辑,这是一个导航条,肯定要一直用的),却又纠结要不要回收它,蛋疼?回过头继续说,以上两者:“活对象”和“死对象”,都不是造成内存泄露的问题,真正需要关注的,是那些“无法追踪的对象”,有时候我们称之为“幽灵对象”。这些对象的特点是:
这样的对象才是会造成内存泄露的罪魁祸首。举个例子:
这是一个 jQuery 插件的部分代码,它的作用是在
$this
(无所谓这是什么,不重要)对象上插入一个span
来充当关闭按钮,然后当span
点击的时候,把$this
收起来。这里的
span
就是我们提到的“幽灵对象”,看下图:这是我们在页面上三次调用此插件后的结果,我们看到 DOM 树里产生了三个一模一样的
<span>
(实际上只用到了一个),并且更要命的是,每一个都绑定了一模一样的匿名的事件回调函数。这才是内存泄露!试想如果这个部分在你的业务逻辑里是需要经常交互的,会产生多少这样无用但是活着的对象以及绑定于其上的事件回调函数对象?这个例子非常简单,对吗?根本就不难理解,但却是在实践中最容易犯的错误!这才是每一位开发者应该关注的地方,别搞错了方向。再强调一遍:没用的活着的对象才是需要回收的垃圾,有用的活着的对象虽然占用了内存,但是不需要被回收,因为它们本该如此!
顺便插一句,如果上例中的回调函数是具名函数,情况会稍微得到缓解,因为所有注册的
click
事件的回调函数都是指向同一个具名函数,而不是每次产生一个匿名函数,这就是为什么很多代码规范里禁止过度使用匿名函数的原因,这也是一条避免内存泄露的最佳实践。上例要如何优化?避免内存泄露不一定非得靠释放或者 null 化,在上例中我们只需要一个
span
就够用了,所以简单的判断其是否存在即可:OK,我举的这个例子貌似不是题主关心的东西,但我真的不是在跑题,而是因为原题的诉求明显是自相矛盾的。如果你要的就是监听
mouseover
然后切换.current
,为什么你要把li
对象回收掉?如果回收掉了,你要怎么继续保持监听?另外一个大家关心的问题是:我这么写到底对不对?那这个问题要从两方面来说,一是对不对,二是好不好。
对不对?代码的实现是 OK 的,能满足业务需求,所以答案很简单:对。
好不好?这个……如果你没有谱,我们来实践一下看看从检查内存占用的角度如何检测你的代码运行效率。
一个前提:占用内存是必须的,因为你的代码总要发挥作用不是?所以别龟毛这个,这个世界不存在“占用内存为 0 且有用的”程序。
然后,我们写一个能实际运行的页面来做测试基准:
我们将要考量两个方面:
核实回调函数的注册是否有效,是否有进一步优化的空间。
查看内存占用:大量执行监听回调,看看内存的上涨情况是否出乎预期。
我先用开发者工具里的 Timeline,观察代码执行情况(如图)
这是从默认页面(我带开了隐匿模式,因为不想让插件出现在这里混淆视听)到载入测试页面前五秒发生的事情。我们能看到什么?Well,信息量很大,不过我们可以观察到以下几件事情:
左上我切换到 Memory,可以看到这个时间段内存变化是平缓的,没有大幅波动
左中 RECORDS 栏:
左下 COUNTERS 栏:
接着,我们继续测量,持续了一分多钟不断触发
mouseover
事件,结果如下图:Memory 开始持续上涨,但是没有明显的波动,这说明我们代码的执行占用是平缓的,没有出乎意料的地方。
可能你比较纠结于这个上涨的图形和
2.1Mb ~ 2.3Mb
变化,但是不要忘了这是在一分钟内疯狂触发了 1100+ 次mouseover
事件之后的结果,实际中谁会无聊到这么做?这是一次极限测试,而不是常规测试,测试的结果是好的,完全不必纠结。中间随机抽取了部分时段的 Event 事件,如果你点开就会看到回调函数执行的占用情况。看一下右边,每一次执行占用的堆大小仅仅是 164B,完全 OK。
下面计数那里没有变化,没有额外的文档、节点以及监听回调产生,perfect!
这是一个非常非常正常的测试页面,实在没什么好担心的(我都不知道花时间在这上面有什么意义?只做这一次!)但是回到最开始的问题:好不好?的确,这段代码有值得改善的空间,也主要集中在两点:
是不是一定要把事件注册在
li
,然后触发的时候再通过遍历li
去处理所有的a
?这不是代码逻辑的问题(当然,短短的代码两次遍历
li
集合也的确糟糕),而是对浏览器事件模型的理解问题。事实上,我们可以只把事件注册给a
集合的共同祖先ul#nav
,然后通过事件委托(冒泡)获取当前事件对象的目标event.target
。这样无需遍历就解决了添加.current
的问题。移除其他的
.current
需要遍历了,但是也不必用到li
(假设你是要把.current
加到a
上面去),直接使用ul#nav
去找就可以。是不是可以使用具名函数来分离代码逻辑,并且减少事件监听回调函数的数目?
当然!我们应该遵循这个公认的最佳实践。首先把两步处理抽离,这样易读易维护;其次减少注册的回调函数,最终我做了以下的修改:
结果如下图:
没什么变化?当然了,原先的代码也不存在内存泄露问题(不过也快了……),当然不会有什么显著的变化。我所做的只是几个小小的代码优化:
这些东西在本例中对于内存占用的优化微乎其微,因为本来就没什么好优化的。
Another thing,也许你应该考虑把
.current
给li
而不是a
,这样在利用ul#nav
向下查找子节点时可以少一层,这点改进看起来太微乎其微了,不过它会给 CSS 带来些许便利,使得选择符及可控灵活性都有一些提升。而最后的代码为了不触发行为在ul#nav
和li
身上,做了保护层,你也可以在这层保护之内继续向下寻找特定的那个a
为它加上.current
,我就直接返回了。在现实里我根本就不会这样设计这段代码。(又及:CSS 调整的好,可以避免e.target
是ul#nav
或li
的情况,鼠标始终只能碰到a
,不过这也是很琐碎的事情,我根本就不会考虑这种实现)小小的一段代码我稀里哗啦写一大堆,实在是没必要,而且最终也没能满足楼主的期望,既要业务实现,还要把
li
回收掉(不过我给出的代码实际上从另外一个角度解决了这个问题,我根本就没有生成li
对象)。不过我希望题主会觉得我写的很多看似跑题的东西是有价值的。以上所有,归根结底是为了表达以下几个点(总结):