JavaScript 全局变量 "let" 在函数中为何不更新?

18

编辑:我已将此报告为Chromium bug:https://bugs.chromium.org/p/chromium/issues/detail?id=668257

我正在使用JS创建一个带有可以射击的敌人的小型画布游戏。为了测试,我创建了一个标志,作为全局声明 let fancy = true;,以确定是否使用“花哨”的目标算法。我使得按下P会切换此标志。我的主要函数frame每秒调用另一个名为autoShoot的函数五次。autoShoot使用fancy 标志。

今天,一些奇怪的事情发生了;我不记得是什么改变引起了它。有时,当我按下P键时,autoShoot的行为就像没有切换fancy一样。我进行了一些调试,并发现新的切换值在frame内反映,但在autoShoot中,该值并未更新。它是间歇性的,并且有时autoShoot中的值会自动修复(而我什么也没做)。

我将代码简化为以下内容,仍然对我显示出问题。试着按下P键多次。对我来说,在仅按下一两次P键后,这两个值就会“不同步”并显示不同:

Screenshot after pressing P on my computer

(我在Windows 10上运行Chrome“版本54.0.2840.99 m”)

const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);

function testSet(id, val) {
  console.log(id+": "+val);
  document.getElementById(id).innerText = val;
}


let fancy = true;
document.body.addEventListener("keydown", function(e) {
  if (e.keyCode == 80) {
    fancy = !fancy;
    console.log("Set fancy to: "+fancy);
  }
});

let bullets = Array(2000);
let lastTime = 0, shotTimer = 0;
function frame(time) {
  const dt = (time - lastTime)/1000;
  lastTime = time;
  
  if ((shotTimer -= dt) <= 0) {
    testSet("frame", fancy);
    autoShoot();
    shotTimer = 0.2;
  }
  for (let b of bullets) {}
  
  requestAnimationFrame(frame);
}
function autoShoot() {
  testSet("autoShoot", fancy);
}

requestAnimationFrame(frame);
<code>
  fancy (frame)     = <span id="frame"></span><br>
  fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>

玩弄一下,以下是一些观察结果:

  • 删除以下任何内容都会使问题消失:
    • 代码顶部处理画布的任何行,甚至是 const ratio 后的注释。
    • 空的 for...of 循环: for (let b of bullets) {}
    • let fancy = 改为 var fancy = 或者只用 fancy =
    • 将整个事物放到全局范围之外(使用 IIFE、onload 处理程序或块作用域)
  • 增加 bullets 数组的大小会增加出现问题的频率。我想这是因为它使 frame 执行时间变长;最初,bullets.length 只有 20,但每次循环迭代都会执行一些更新子弹等操作。

这在您的计算机上是否发生?有没有什么逻辑解释?我试过重新启动浏览器,但没有改变。


2
不,它在Firefox中永远不会失去同步。 - Jaromanda X
2
在 Firefox 和 Chrome 中,这个 fiddle 对我来说非常稳定。 - Pointy
2
我可以重现。autoShoot需要一些时间来跟随更新。很奇怪。 - Bergi
2
@Ultimater,我不再担心让它工作了;我只是将“let”换成了“var”。现在我只是好奇为什么会发生这种情况。 - qxz
2
已报告:https://bugs.chromium.org/p/chromium/issues/detail?id=668257 - qxz
显示剩余19条评论
2个回答

2
因为大家都提到了,这似乎是一个Chrome问题。
我已经尝试在Chrome版本45.0.2454.85 m(64位)和44.0.2403.107 m(32位)上复制相同的问题(当然启用了严格模式),但我没有成功。但在版本54.0.2840.99 m(64位)中存在该问题。
我注意到将requestAnimationFrame更改为类似于setInterval的东西也会使问题完全消失。
所以,我认为这种奇怪的行为与Chrome的requestAnimationFrame在较新版本上有关,也可能与let的块作用域性质和函数提升有关。

我无法确定从哪个版本的Chrome开始出现这种“错误”,但我可以猜测可能是版本52,因为在这个版本中发生了许多变化,比如新的Garbage collection方法、对es6es7的本地支持等。更多信息请查看来自I/O 2016的视频

也许,新的Garbage collection方法导致了这个问题,因为正如他们在上述视频中所说,它与浏览器框架有关,类似于v8在浏览器空闲时进行GC,以避免影响帧的绘制等。而且我们知道,requestAnimationFrame是一种在下一帧绘制时调用callback的方法,也许在这个过程中发生了奇怪的事情。但这只是一种猜测,我没有能力说出什么严肃的话:)


1
脚本中不应该产生任何垃圾,所有内容都可以静态分配(或在堆栈上),因此没有需要收集的东西。 - Bergi

0

我正在使用Mac上的Chrome 54.0.2840.98,它也会发生。我认为这是一个作用域问题,因为如果我将let语句后面的声明包装在{...}块中,那么片段就可以正常工作,按键后两个值立即改变。

const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);

function testSet(id, val) {
  console.log(id+": "+val);
  document.getElementById(id).innerText = val;
}


let fancy = true;
{
  document.body.addEventListener("keydown", function(e) {
    if (e.keyCode == 80) {
      fancy = !fancy;
      console.log("Set fancy to: "+fancy);
    }
  });

  let bullets = Array(2000);
  let lastTime = 0, shotTimer = 0;
  function frame(time) {
    const dt = (time - lastTime)/1000;
    lastTime = time;
  
    if ((shotTimer -= dt) <= 0) {
      testSet("frame", fancy);
      autoShoot();
      shotTimer = 0.2;
    }
    for (let b of bullets) {}
  
    requestAnimationFrame(frame);
  }
  function autoShoot() {
    testSet("autoShoot", fancy);
  }

  requestAnimationFrame(frame);
}
<code>
  fancy (frame)     = <span id="frame"></span><br>
  fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>


为什么不将 let fancy 也放在块级作用域内呢? - Bergi
“*Without the block scope it's limited to the keydown listener only.*” 这句话的意思是什么?有什么限制? - Bergi
@Bergi 很好的问题 ;) 我不是ES6的专家,但我知道let语句与块作用域有关。我的方法就是基于这个,所以我在其中进行了尝试。 - RWAM
2
是的,楼主在尝试不同的解决方案时避免了这个问题,但他想要的是对观察到的行为的解释。 - Bergi
尽管这个答案没有解释它,但有趣的是,即使let仍然在全局范围内,问题也会消失... - qxz

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