谷歌浏览器的JavaScript控制台在评估对象方面是否存在惰性?

174

我将从代码开始:

var s = ["hi"];
console.log(s);
s[0] = "bye";
console.log(s);

简单吧?对此,Firefox控制台显示:

[ "hi" ]
[ "bye" ]

很好,但是Chrome的JavaScript控制台(7.0.517.41 beta)显示:

[ "bye" ]
[ "bye" ]

我做错了什么吗?还是Chrome的JavaScript控制台在评估我的数组时异常懒惰?

控制台截图,展示了所描述的行为。


1
我在Safari中观察到了相同的行为 - 所以这可能是一个webkit的问题。非常令人惊讶。我会称其为一个bug。 - Lee
7
在我看来,这似乎是一个错误。在Linux系统上,Opera和Firefox显示了预期的结果,而Chrome和其他基于Webkit的浏览器没有显示。您可能需要向Webkit开发人员报告此问题:http://webkit.org/quality/reporting.html - tec
2
截至2016年3月,这个问题已经不存在了。 - kmonsoor
3
2020年4月,在Chrome中遇到这个问题。浪费了2个小时在我的代码中寻找一个错误,结果发现是Chrome的一个错误。 - The Fox
1
值得注意的是,蓝色的“i”图标的工具提示显示“下面的值刚刚被评估”。 - Sebastian Simon
显示剩余10条评论
7个回答

95

感谢你的评论,tec。我能够找到一个现有的未经确认的Webkit bug来解释这个问题:https://bugs.webkit.org/show_bug.cgi?id=35801(编辑:现在已经修复!)

关于这个bug有一些争议,就它到底算不算bug、是不是可以修复等等问题。对我来说,这似乎是一种不好的行为。特别是在Chrome中,即使控制台打开时,当代码存在于立即执行的脚本中并且页面重新加载时,它也会发生。当控制台尚未激活时调用console.log仅导致将对象引用排队,而不是控制台将包含的输出。因此,数组(或任何对象)直到控制台准备好才被评估。这真的是一种惰性评估的情况。

但是,在您的代码中有一种简单的方法可以避免这种情况:

var s = ["hi"];
console.log(s.toString());
s[0] = "bye";
console.log(s.toString());

调用toString方法,可以创建一个在接下来的语句中不会改变的内存表示,在控制台准备好时将被读取。 控制台输出与直接传递对象略有不同,但似乎可以接受:

hi
bye

2
实际上,对于关联数组或其他对象来说,这可能是一个真正的问题,因为toString不会产生任何有价值的东西。是否有一种通用的简单解决方法? - Eric Mickelsen
1
几个月前,WebKit已经为此提交了一个补丁。 - antony.trupe
4
请执行以下操作: console.log(JSON.parse(JSON.stringify(s))); 该操作表示将对象 s 转化为字符串,再将其转化回对象并打印在控制台上。 - Lee Comstock
1
我只是想提一下,在当前的Chrome版本中,控制台再次延迟并且输出值错误(或者它曾经正确过)。例如,我正在记录一个数组并在记录后弹出顶部值,但它显示的是没有弹出的值。您的toString()建议对我非常有帮助,让我看到了需要查看的值。 - Nicholas R. Grant
通过在代码中插入 debugger; 来设置断点也是一个很好的选择(如果可行的话,也可以手动从开发者工具中添加断点)。 - Giorgio Tempesta

31

根据Eric的解释,这是由于console.log()被排队了,它会打印数组(或对象)的后续值。

有以下5种解决方案:

1. arr.toString()   // not well for [1,[2,3]] as it shows 1,2,3
2. arr.join()       // same as above
3. arr.slice(0)     // a new array is created, but if arr is [1, 2, arr2, 3] 
                    //   and arr2 changes, then later value might be shown
4. arr.concat()     // a new array is created, but same issue as slice(0)
5. JSON.stringify(arr)  // works well as it takes a snapshot of the whole array 
                        //   or object, and the format shows the exact structure

任何复制列表/对象的解决方案都可以。我最喜欢的对象浅拷贝自ECMAScript 2018以来就可用了:copy = {...orig} - Scar
1
@Scar 值得一提的是,你进行浅拷贝时会将数组转换为对象。 - Krismu

7
您可以使用Array#slice克隆数组:
console.log(s); // ["bye"], i.e. incorrect
console.log(s.slice()); // ["hi"], i.e. correct

一个可以代替 console.log 的函数,不会出现这个问题,如下所示:

console.logShallowCopy = function () {
    function slicedIfArray(arg) {
        return Array.isArray(arg) ? arg.slice() : arg;
    }

    var argsSnapshot = Array.prototype.map.call(arguments, slicedIfArray);
    return console.log.apply(console, argsSnapshot);
};

对于对象的情况,不幸的是,最好的方法似乎是先使用非 WebKit 浏览器进行调试,或编写一个复杂的克隆函数。如果您只处理简单的对象,其中键的顺序不重要且没有函数,则可以始终执行以下操作:

console.logSanitizedCopy = function () {
    var args = Array.prototype.slice.call(arguments);
    var sanitizedArgs = JSON.parse(JSON.stringify(args));

    return console.log.apply(console, sanitizedArgs);
};

所有这些方法明显都很慢,因此即使在使用普通的console.log时,你也必须在完成调试后将它们删除。

5
这个问题已经在Webkit中得到修复,但是在使用React框架时,在某些情况下我也会遇到这个问题,如果你也遇到类似的问题,可以像其他人建议的那样操作:
console.log(JSON.stringify(the_array));

3
可以确认,当尝试记录ReactSyntheticEvents时,这确实是最糟糕的。即使使用JSON.parse(JSON.stringify(event))也无法获得正确的深度/准确性。调试器语句是我找到的唯一真正解决此问题的方法。 - CStumph

2

看起来Chrome在“预编译”阶段会将“s”的所有实例替换为指向实际数组的指针

解决方法之一是克隆数组,记录新的副本:

var s = ["hi"];
console.log(CloneArray(s));
s[0] = "bye";
console.log(CloneArray(s));

function CloneArray(array)
{
    var clone = new Array();
    for (var i = 0; i < array.length; i++)
        clone[clone.length] = array[i];
    return clone;
}

对象,你是指这个吗?var s = { param1: "hi", param2: "how are you?" }; 如果是的话,我刚刚测试了一下,当你有s["param1"] = "bye";时,它按预期正常工作。你能否举一个“它对对象不起作用”的例子?我会尝试解决这个问题。 - Shadow The Spring Wizard
@Domenic 因为当时我不熟悉 slice - Shadow The Spring Wizard
很好,我用基于“切片”的解决方案接管了Anonymous的答案。 - Domenic
@Dom为什么不创建一个新的Answer呢?:) - Shadow The Spring Wizard
好的,它已经是“slice”了,但它需要一些ESL和清晰度修正。主要内容仍然是神秘匿名先生的,所以我想做一个编辑。 - Domenic
显示剩余4条评论

2

目前最简单的解决方案是使用数组或对象扩展语法来获取需要保留的值的克隆,以便在记录时保留其状态,即:

console.log({...myObject});
console.log([...myArray]);

但是需要注意的是,它只进行浅复制,因此任何深度嵌套的非原始值都不会被克隆,因此在控制台中显示它们的修改状态。


不错的想法,但它只适用于一层深度的数据,对于每个更深层次的嵌套对象/数组来说则无法实现。 - Radon8472
不错的想法,但它只适用于一层深度的数据,对于每个更深层次的嵌套对象/数组,它会失败。 - undefined

1
这个问题已经有答案了,但我还是会提供我的答案。我实现了一个简单的控制台包装器,它不会遇到这个问题。需要使用jQuery。
它只实现了 logwarnerror 方法,你需要添加更多方法以便能够替换常规的 console
var fixedConsole;
(function($) {
    var _freezeOne = function(arg) {
        if (typeof arg === 'object') {
            return $.extend(true, {}, arg);
        } else {
            return arg;
        }
    };
    var _freezeAll = function(args) {
        var frozen = [];
        for (var i=0; i<args.length; i++) {
            frozen.push(_freezeOne(args[i]));
        }
        return frozen;
    };
    fixedConsole = {
        log: function() { console.log.apply(console, _freezeAll(arguments)); },
        warn: function() { console.warn.apply(console, _freezeAll(arguments)); },
        error: function() { console.error.apply(console, _freezeAll(arguments)); }
    };
})(jQuery);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接