迭代全局对象的键尝试访问异步回调中的未定义变量

时间:2017-07-14 20:06:01

标签: javascript arrays node.js

if (Object.keys(globalObject).length != 0) {
  Object.keys(globalObject).forEach(function(key) {
    database.query('SELECT * from accounts WHERE `ID` = ' + key + ' LIMIT 1', function(error, rows, fields) {
      if (rows.length == 0) return;
      console.log('User has  ' + globalObject[key].length + ' items')
      globalObject[key].forEach(function(item) {
        console.log('Item ID is ' + item.id)
      });
      delete globalObject[key]
    })
  })
}

globalObject示例(根据连接的用户和每个用户项不断变化):

globalObject = { "1001":[{id:1},{id:2}], "1002":[{id:2},{id:3}]}

有时我会遇到致命的错误,崩溃:

TypeError: Cannot read property 'length' of undefined

在这一行:

console.log('User has  ' + globalObject[key].length + ' items)

尽管在程序的不同部分随时推送不同的用户,但我删除globalObject [key]的唯一地方是在我处理完用户之后。

那么当我通过迭代器(forEach)访问它时,如果一个密钥不存在(未定义),那么为什么密钥存在并且直到最后它才会被删除?

编辑:

我认为原因是我在setInterval(每200ms)内调用forEach,所以在forEach完成之前它会再次被调用,因此删除了密钥。如何让它更加同步,以避免在很短的时间内为同一个键调用两次?

1 个答案:

答案 0 :(得分:0)

最有可能的情况是,您一次运行多个这样的循环并且会创建潜在的竞争条件(因为异步数据库调用),其中一个循环删除另一个循环仍在处理的密钥

最好的解决方法是避免同时运行其中两个,并防止可能的竞争条件。如果你向我们展示调用代码上下文,那么我们可以看到如何在另一个完成之前多次调用它,我们可以建议一种方法来避免这种情况。

即使没有这个,你也可以防止这样的竞争条件:

  Object.keys(globalObject).forEach(function(key) {
    database.query('SELECT * from accounts WHERE `ID` = ' + key + ' LIMIT 1', function(error, rows, fields) {
      // if the db says no rows or the key is already gone, then nothing to do
      if (rows.length == 0 || !globalObject[key]) return;
      console.log('User has  ' + globalObject[key].length + ' items')
      globalObject[key].forEach(function(item) {
        console.log('Item ID is ' + item.id)
      });
      delete globalObject[key];
    });
  });

添加到!globalObject[key]语句的额外if检查可以防止其他人在您的数据库调用期间删除了密钥。

顺便说一下,您不需要if

if (Object.keys(globalObject).length != 0) { 

因为如果数组是空的Object.keys(globalObject).forEach()可以正常工作(它只是无关)。

根据您的评论做出更多解释:

.forEach()循环运行完成而不会中断。但是循环中的数据库操作是非阻塞的。他们开始了,但循环不等他们完成。因此,.forEach()循环将完成,但循环内的回调尚未被调用。

因此,如果您有10个密钥,则将启动10个数据库操作(尚未完成)。现在,.forEach()循环完成了。如果你的setInterval()在所有10个数据库操作完成之前再次调用此代码,那么将运行一个新的.forEach()循环,并根据即将被第一个循环删除的键启动更多的数据库操作(当它的数据库操作最终完成时)。现在,在第二个.forEach()循环运行后,第一个循环的数据库操作开始完成并删除键。然后,在完成后,将调用第二个.forEach()循环完成的数据库操作及其回调,并且他们正在尝试使用现在已被第一个循环中的回调删除的键。

循环本身不会同时运行。但是,在每个循环运行之后,它会设置在未来某个不确定时间运行的数据库回调,并且那些回调(由您编码)假设某些键组尚未被删除。但是,如果您没有等到第一个循环中的所有数据库回调都在开始第二个循环之前完成,那么该假设就会分崩离析,因为第二个循环将使用第一个循环的数据库调用将要删除的键。终于完成了。这将搞乱第二个循环启动的数据库回调。

.forEach()来电与事件无关。它们只是Javascript中的循环,它们同步运行。数据库调用只是启动异步操作。他们的回调将在未来的某个时间由事件触发。因为这些数据库调用是非阻塞异步的,所以.forEach()循环结束,然后JS解释器获取触发数据库回调的事件。