Javascript 导致大量内存泄漏

Javascript is causing MASSIVE memory leaks

在过去的几天里,我们家的电脑意外地突然死机了很多次。恢复需要 UI 大约半小时,有时需要硬重启。打开任务管理器显示一个特定的 PID 正在使用大约 96% 的 PC 内存,并且使用 Chrome 任务管理器,我确定它是我制作的网页。

我对内存泄漏知之甚少,因为我只是顺便听说过它们,但我几乎可以肯定我的 Javascript 代码以某种方式导致了内存泄漏。是否有修改代码以阻止内存泄漏的最佳实践?我怀疑泄漏是由我的 response() 函数引起的。

在这个问题因重复或未提供研究而被否决之前,this other Stack Overflow question 让我相信 case "12124561414": 可能是罪魁祸首;但是,我无法对此进行测试。此外,这个函数中包含的循环没有执行,所以我不确定这是否真的是罪魁祸首,因为我不知道有任何内置的 JS 机制来隔离内存泄漏。

有什么方法可以运行无限循环而不导致内存泄漏吗? (如果这确实是我遇到巨大内存泄漏的原因)或某种方式来释放这些显然 被释放的资源?我的目标是让忙音只是 运行 一遍又一遍直到页面刷新为止,但我认为没有理由让计算机崩溃。

本网页完整的JS代码如下:

    var availableNumbers = ["0", "911", "1 (847) 765-1008" , "867-5309", "1 (212) 456-1414", "555-1212", "555-5555"];
    function numberSuggestion() {
        var randomNumber = Math.floor(Math.random() * (availableNumbers.length));
        var suggestedNumber = availableNumbers[randomNumber];
        document.getElementById("suggestion").innerHTML = "How about dialing <strong id='suggestedTelephoneNumber'>" + suggestedNumber + "</strong>? Don't like this number? Click the button above again!";
    }
    var dialTone;
    function offHook() {
        document.getElementById("WE2500").style.display = "none";
        document.getElementById("dialPad").style.display = "block";
        dialTone = new Audio('dialTone.m4a');
        dialTone.play();
    }
    var number = "";
    var timeout;
    function numberDial() {
        if (dialTone) {
            dialTone.pause();
            dialTone.currentTime = 0;
        }
        clearTimeout(timeout);
        timeout = setTimeout(dial, 2000);
    }
    function dial1() {
        numberDial();
        number = number + "1";
        var tone1 = new Audio('DTMF-1.wav');
        tone1.play();
    }
    function dial2() {
        numberDial();
        number = number + "2";
        var tone2 = new Audio('DTMF-2.wav');
        tone2.play();
    }
    function dial3() {
        numberDial();
        number = number + "3";
        var tone3 = new Audio('DTMF-3.wav');
        tone3.play();
    }
    function dial4() {
        numberDial();
        number = number + "4";
        var tone4 = new Audio('DTMF-5.wav');
        tone4.play();
    }
    function dial5() {
        numberDial();
        number = number + "5";
        var tone5 = new Audio('DTMF-5.wav');
        tone5.play();
    }
    function dial6() {
        numberDial();
        number = number + "6";
        var tone6 = new Audio('DTMF-6.wav');
        tone6.play();
    }
    function dial7() {
        numberDial();
        number = number + "7";
        var tone7 = new Audio('DTMF-7.wav');
        tone7.play();
    }
    function dial8() {
        numberDial();
        number = number + "8";
        var tone8 = new Audio('DTMF-8.wav');
        tone8.play();
    }
    function dial9() {
        numberDial();
        number = number + "9";
        var tone9 = new Audio('DTMF-9.wav');
        tone9.play();
    }
    function dial0() {
        numberDial();
        number = number + "0";
        var tone0 = new Audio('DTMF-0.wav');
        tone0.play();
    }
    function dialStar() {
        numberDial();
        number = number + "*";
        var toneStar = new Audio('DTMF-star.wav');
        toneStar.play();
    }
    function dialPound() {
        numberDial();
        number = number + "#";
        var tonePound = new Audio('DTMF-pound.wav');
        tonePound.play();
    }
    var ringingTone = new Audio('DTMF-ringbackTone.mp3');
    var timesRung = 0;
    function dial() {
        function ring() {
            ringingTone.play();
            timesRung++;
            if (timesRung > 1) {
                setTimeout(response, 700);
            }
        }
        ring();
        setTimeout(ring, 4000);
    }
    function response() {
        switch(number) {
            case "0":
                var operatorPickup = new Audio('OperatorAnswer.wav');
                operatorPickup.addEventListener("ended", function(){
                    number = prompt("Operator, your number please? (Numbers only; enter 'police' for police and emergency)");
                    if (number == null) {
                        number = "0";
                    }
                    operatorPutCallThrough();
                });
                operatorPickup.play();
                break;
            case "911":
                var pickup911 = new Audio('911-xxx-fleet.mp3');
                pickup911.play();
                break;
            case "18477651008":
                var pickupMCI = new Audio('MCI.wav');
                pickupMCI.play();
                break;
            case "8675309":
                var pickup8675309 = new Audio('discoornis-bell-f1.mp3');
                pickup8675309.play();
                break;
            case "12124561414":
                var pickupBusy = new Audio('tele-busy.wav');
                console.log(number);
                while (number == "12124561414") {
                    pickupBusy.play();
                    pickupBusy.currentTime = 0;
                }
                break;
            case "5551212":
                var pickupLocalInfo = new Audio('tele-busy.wav');
                pickupLocalInfo.play();
                break;
            case "5555555":
                var pickup5555555 = new Audio('timeout-bell-f1.mp3');
                pickup5555555.play();
                break;
            case "police":
                break;
            default:
                var pickupDefault = new Audio('ldcircuits-bell-f1.mp3');
                pickupDefault.play();
        }
    }
    function operatorPutCallThrough() {
        alert("One moment please, ringing the line now.");
        if (number == "police") {
            var operatorPoliceTransfer = new Audio('OperatorPolice.wav');
            operatorPoliceTransfer.play();
        }
        response();
    }

我的直觉是这可能是更好的方法,但我有点害怕测试它以防 PC 再次崩溃。

            case "12124561414":
                busy();
                break;
            case "5551212":
                busy();
                break;
            case "5555555":
                var pickup5555555 = new Audio('timeout-bell-f1.mp3');
                pickup5555555.play();
                break;
            case "police":
                break;
            default:
                var pickupDefault = new Audio('ldcircuits-bell-f1.mp3');
                pickupDefault.play();
        }
    }
    function busy() {
        function() busyTone {
            var pickupBusy = new Audio('tele-busy.wav');
            pickupBusy.play();
            pickupBusy.currentTime = 0;
        }
        setInterval(function() busyTone, 1);
    }

最重要的是,您似乎对无限循环感到困惑。当您输入一个时,某些浏览器 挂起。例如,将此输入到您的 JS 控制台中,您可能会看到类似于您的挂起:

var number = "12124561414";
while (number == "12124561414") { /* do nothing */ }

这里的问题是 while 循环与 GUI 循环在同一个线程中运行,因此 GUI 将锁定直到循环停止。

此问题的解决方案以与您在此处提供的其他代码片段类似的形式出现。例如,此代码在一段时间后播放一次音调,允许 GUI 在该时间段之间更新:

function numberDial() {
    if (dialTone) {
        dialTone.pause();
        dialTone.currentTime = 0;
    }
    clearTimeout(timeout);
    timeout = setTimeout(dial, 2000);
}

改为使用 setInterval,您将每 2 秒调用一次 dial,直到您使用 clearInterval 停止它,允许 GUI 循环继续其工作(刷新用户界面)直到两秒过去。


回答您的问题:

Is there a best practice to modify code to stop memory leaks?

在设计您的软件时投入大量的批判性思考,这样对象就不会不必要地闲逛。

Is there any way to run indefinite loops without causing memory leaks?

我认为 Rust doc 很好地回答了这个问题...

... It's quite trivial to initialize a collection at the start of a program, fill it with tons of objects with destructors, and then enter an infinite event loop that never refers to it. The collection will sit around uselessly, holding on to its precious resources until the program terminates (at which point all those resources would have been reclaimed by the OS anyway).

换句话说,这取决于循环,以及循环之前发生的事情!

让我们考虑一下在您的(浏览器挂起的)循环之前这行代码中发生了什么:console.log(number);。我相信 this 是您确定的代码中 唯一可能的泄漏 ,因为这会导致对 number 的引用,因为一个对象,要存储到这样的集合中(debug 日志)。

进一步分析表明,这可能 不是 泄漏,因为 number 无论如何都在函数外部声明...值得但是请注意,console.log 可以防止对象被视为垃圾!

Or some way to release these resources that are apparently not being released?

我建议确保垃圾收集器可以通过有条件地记录以减少存储量来回收这些资源。事实上,您推送到日志的引用也将存储其他引用(例如返回到全局对象)。