为什么我的变量在函数内部修改后没有改变?- 异步代码参考

IT技术 javascript asynchronous
2020-12-10 21:09:23

鉴于以下示例,为什么outerScopeVar在所有情况下都是undefined?

var outerScopeVar;

var img = document.createElement('img');
img.onload = function() {
    outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);

var outerScopeVar;
setTimeout(function() {
    outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);

// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
});
alert(outerScopeVar);

// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
    outerScopeVar = data;
});
console.log(outerScopeVar);

// with promises
var outerScopeVar;
myPromise.then(function (response) {
    outerScopeVar = response;
});
console.log(outerScopeVar);

// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

为什么它会undefined在所有这些示例中输出我不想要解决方法,我想知道为什么会这样。


注意:这是JavaScript 异步性的规范问题随意改进这个问题并添加社区可以识别的更多简化示例。

6个回答

一句话回答:异步

前言

这个话题在 Stack Overflow 上至少被重复了几千次。因此,首先我想指出一些非常有用的资源:


手头问题的答案

让我们首先追踪常见的行为。在所有示例中,outerScopeVar都在函数内部被修改该函数显然不会立即执行,而是被分配或作为参数传递。这就是我们所说的回调

现在的问题是,该回调何时调用?

视情况而定。让我们再次尝试追踪一些常见的行为:

  • img.onload可能会在将来的某个时间调用,当(以及如果)图像已成功加载时。
  • setTimeout可能会在未来某个时间调用,在延迟到期并且超时没有被 取消之后clearTimeout注意:即使使用0作为延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4ms)。
  • jQuery$.post的回调可能会在未来某个时间调用,当(以及如果)Ajax 请求成功完成时。
  • 当文件被成功读取或抛出错误时,Node.jsfs.readFile可能会在未来某个时候被调用

在所有情况下,我们都有一个可能在未来某个时间运行的回调这个“未来某个时候”就是我们所说的异步流

异步执行被推出同步流程。也就是说,异步代码永远不会在同步代码堆栈执行时执行。这就是 JavaScript 是单线程的含义。

更具体地说,当 JS 引擎空闲时——不执行一堆(a)同步代码——它将轮询可能触发异步回调的事件(例如超时超时、收到网络响应)并一个接一个地执行它们。这被视为事件循环

也就是说,手绘红色形状中突出显示的异步代码只有在其各自代码块中的所有剩余同步代码都执行完毕后才能执行:

异步代码突出显示

简而言之,回调函数是同步创建但异步执行的。在您知道异步函数已执行之前,您不能依赖它的执行,以及如何做到这一点?

这很简单,真的。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,在回调函数中移动alerts 和console.logs 会输出预期的结果,因为此时结果可用。

实现自己的回调逻辑

通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们处理一个更复杂的例子:

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

注意:我使用setTimeout随机延迟作为通用异步函数,同样的示例适用于 Ajax readFileonload和任何其他异步流。

这个例子显然与其他例子有同样的问题,它不会等到异步函数执行。

让我们来解决它实现我们自己的回调系统。首先,我们摆脱了outerScopeVar在这种情况下完全没用的丑陋然后我们添加一个接受函数参数的参数,我们的回调。当异步操作完成时,我们调用此回调传递结果。实现(请按顺序阅读评论):

// 1. Call helloCatAsync passing a callback function,
//    which will be called receiving the result from the async operation
helloCatAsync(function(result) {
    // 5. Received the result from the async function,
    //    now do whatever you want with it:
    alert(result);
});

// 2. The "callback" parameter is a reference to the function which
//    was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
    // 3. Start async operation:
    setTimeout(function() {
        // 4. Finished async operation,
        //    call the callback passing the result as argument
        callback('Nya');
    }, Math.random() * 2000);
}

上面例子的代码片段:

// 1. Call helloCatAsync passing a callback function,
//    which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
    // 5. Received the result from the async function,
    //    now do whatever you want with it:
    console.log("5. result is: ", result);
});

// 2. The "callback" parameter is a reference to the function which
//    was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
    console.log("2. callback here is the function passed as argument above...")
    // 3. Start async operation:
    setTimeout(function() {
    console.log("3. start async operation...")
    console.log("4. finished async operation, calling the callback, passing the result...")
        // 4. Finished async operation,
        //    call the callback passing the result as argument
        callback('Nya');
    }, Math.random() * 2000);
}

大多数情况下,在实际用例中,DOM API 和大多数库已经提供了回调功能(helloCatAsync本演示示例中实现)。您只需要传递回调函数并了解它将在同步流之外执行,并重构您的代码以适应它。

您还会注意到,由于异步性质,不可能将return异步流中的值返回到定义回调的同步流,因为异步回调在同步代码已经完成执行后很长时间才执行。

return您将不得不使用回调模式,或者...Promise,而不是从异步回调中获取值。

Promise

尽管有一些方法可以通过 vanilla JS避免回调地狱,但 promise 越来越受欢迎,并且目前正在 ES6 中标准化(请参阅Promise - MDN)。

Promises(又名 Futures)提供了一种更线性的、因此令人愉快的异步代码阅读方式,但解释它们的整个功能超出了这个问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:


更多关于 JavaScript 异步性的阅读材料


注意:我已将此答案标记为 Community Wiki,因此任何拥有至少 100 个声望的人都可以对其进行编辑和改进!请随时改进此答案,或者如果您愿意,也可以提交一个全新的答案。

我想把这个问题变成一个规范的主题来回答与 Ajax 无关的异步问题(有如何从 AJAX 调用返回响应?为此),因此这个主题需要你的帮助尽可能好和有帮助!

@Fabricio 我已经寻找定义“>=4ms 钳位”的规范,但找不到它 - 我在 MDN 上发现了一些类似的机制(用于钳制嵌套调用) - developer.mozilla.org/en-US/docs /Web/API/... - 有没有人有指向 HTML 规范正确部分的链接。
2021-02-07 21:09:23
代码示例有点奇怪,因为您在调用它之后声明了该函数。当然是因为起重工作,但这是故意的吗?
2021-02-15 21:09:23
是否陷入僵局。felix kling 指着你的答案,你指着 felix 的答案
2021-02-20 21:09:23
您需要了解红色圆圈代码只是异步的,因为它是由 NATIVE 异步 javascript 函数执行的。这是你的 javascript 引擎的一个特性——无论是 Node.js 还是浏览器。它是异步的,因为它作为“回调”传递给本质上是一个黑匣子的函数(用 C 等实现)。对于不幸的开发人员来说,他们是异步的……只是因为。如果你想编写自己的异步函数,你必须通过将它发送到 SetTimeout(myfunc,0) 来破解它。你应该这样做吗?另一场辩论......可能不是。
2021-02-20 21:09:23
在你的最后一个例子中,你使用匿名函数是否有特定的原因,或者使用命名函数会产生相同的效果吗?
2021-03-06 21:09:23

Fabrício 的回答恰到好处;但我想用技术性较低的东西来补充他的答案,它侧重于一个类比来帮助解释异步性的概念


一个比喻...

昨天,我正在做的工作需要一位同事提供一些信息。我给他打了电话;谈话是这样进行的:

:嗨鲍勃,我需要知道我们如何“d的酒吧”上周天。吉姆想要一份关于它的报告,而你是唯一知道它细节的人。

鲍勃:当然可以,但我需要大约 30 分钟?

:那是伟大的鲍勃。得到信息后给我回电!

说到这里,我挂了电话。因为我需要 Bob 的信息来完成我的报告,所以我离开了报告并去喝咖啡,然后我赶上了一些电子邮件。40 分钟后(Bob 很慢),Bob 回电并给了我我需要的信息。在这一点上,我继续我的报告工作,因为我拥有了我需要的所有信息。


想象一下,如果谈话变成这样;

:嗨鲍勃,我需要知道我们如何“d的酒吧”上周天。吉姆想要一份关于它的报告,而你是唯一知道它细节的人。

鲍勃:当然可以,但我需要大约 30 分钟?

:那是伟大的鲍勃。我会等待。

我坐在那里等着。并等待。并等待。40 分钟。除了等待什么都不做。最后,鲍勃给了我信息,我们挂了电话,我完成了报告。但我失去了 40 分钟的生产力。


这是异步与同步行为

这正是我们问题中所有示例中发生的情况。加载图像、从磁盘加载文件以及通过 AJAX 请求页面都是缓慢的操作(在现代计算的上下文中)。

等待这些慢速操作完成不同,JavaScript 允许您注册一个回调函数,该函数将在慢速操作完成时执行。然而,与此同时,JavaScript 将继续执行其他代码。JavaScript在等待缓慢操作完成的同时执行其他代码的事实使行为异步如果 JavaScript 在执行任何其他代码之前等待操作完成,这将是同步行为。

var outerScopeVar;    
var img = document.createElement('img');

// Here we register the callback function.
img.onload = function() {
    // Code within this function will be executed once the image has loaded.
    outerScopeVar = this.width;
};

// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);

在上面的代码中,我们要求 JavaScript 加载lolcat.png,这是一个缓慢的操作。一旦这个缓慢的操作完成,回调函数将被执行,但与此同时,JavaScript 将继续处理下一行代码;alert(outerScopeVar)

这就是我们看到警报显示的原因undefined因为alert()是立即处理的,而不是在图像加载后处理。

为了修复我们的代码,我们所要做的就是将alert(outerScopeVar)代码移动回调函数中。因此,我们不再需要将outerScopeVar变量声明为全局变量。

var img = document.createElement('img');

img.onload = function() {
    var localScopeVar = this.width;
    alert(localScopeVar);
};

img.src = 'lolcat.png';

总是看到回调被指定为一个函数,因为这是 JavaScript 中定义一些代码的唯一*方式,但直到以后才执行它。

因此,在我们所有的例子中,function() { /* Do something */ }都是回调;要修复所有示例,我们要做的就是将需要操作响应的代码移到那里!

* 从技术上讲,您也可以使用eval(),但为此目的eval()是邪恶


如何让来电者保持等待?

您目前可能有一些与此类似的代码;

function getWidthOfImage(src) {
    var outerScopeVar;

    var img = document.createElement('img');
    img.onload = function() {
        outerScopeVar = this.width;
    };
    img.src = src;
    return outerScopeVar;
}

var width = getWidthOfImage('lolcat.png');
alert(width);

然而,我们现在知道这return outerScopeVar会立即发生;onload回调函数更新变量之前。这导致getWidthOfImage()返回undefined,并且undefined被警告。

为了解决这个问题,我们需要允许函数调用getWidthOfImage()注册一个回调,然后将宽度的警报移动到该回调内;

function getWidthOfImage(src, cb) {     
    var img = document.createElement('img');
    img.onload = function() {
        cb(this.width);
    };
    img.src = src;
}

getWidthOfImage('lolcat.png', function (width) {
    alert(width);
});

...和以前一样,请注意我们已经能够删除全局变量(在本例中为width)。

这是整个 stackoverflow 上最有用、最快速、最简洁的答案。谢谢。
2021-02-13 21:09:23
但是,如果您想在不同的计算中使用结果或将其存储在对象变量中,那么警报或发送到控制台有何用处?
2021-02-18 21:09:23

对于正在寻找快速参考以及一些使用 promise 和 async/await 的示例的人,这里有一个更简洁的答案。

从调用异步方法(在本例中setTimeout)并返回消息的函数的幼稚方法(不起作用)开始

function getMessage() {
  var outerScopeVar;
  setTimeout(function() {
    outerScopeVar = 'Hello asynchronous world!';
  }, 0);
  return outerScopeVar;
}
console.log(getMessage());

undefined在这种情况下被记录,因为getMessagesetTimeout调用回调和更新之前返回outerScopeVar

解决它的两种主要方法是使用回调Promise

回调

这里的变化是getMessage接受一个callback参数,一旦可用参数将被调用以将结果传递回调用代码。

function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});

Promise

Promise 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合以协调多个异步操作。Promise/ A +标准实现在node.js中(0.12+)和许多当前的浏览器本身提供,但是在像库还实现蓝鸟Q

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

getMessage().then(function(message) {
  console.log(message);  
});

jQuery延迟

jQuery 提供的功能类似于带有 Deferred 的 promise。

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

异步/等待

如果您的 JavaScript 环境支持asyncawait(如 Node.js 7.6+),那么您可以在async函数内同步使用 promise

function getMessage () {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('Hello asynchronous world!');
        }, 0);
    });
}

async function main() {
    let message = await getMessage();
    console.log(message);
}

main();
@InteXX 我不确定你.Get()打电话的意思可能最好发布一个新问题。
2021-02-07 21:09:23
您关于 Promises 的示例基本上是我过去几个小时一直在寻找的内容。你的例子很漂亮,同时解释了Promise。为什么这不是其他任何地方令人难以置信。
2021-02-09 21:09:23
@Chiwda你只要把回调参数最后:function getMessage(param1, param2, callback) {...}
2021-02-09 21:09:23
这一切都很好,但是如果您需要使用参数调用 getMessage() 怎么办?在这种情况下,您将如何编写上述内容?
2021-02-11 21:09:23
我正在尝试您的async/await示例,但遇到了问题。new Promise.Get()没有实例化 a 而是进行调用,因此无法访问任何resolve()方法。因此,我getMessage()返回的是 Promise 而不是结果。您能否稍微编辑一下您的答案以显示此操作的有效语法?
2021-02-16 21:09:23

显而易见,杯子代表outerScopeVar

异步函数就像...

咖啡的异步调用

如果它说的是显而易见的,我认为不会有人问这个问题,不是吗?
2021-02-12 21:09:23
@broccoli2000 我并不是说这个问题很明显,但很明显杯子在图中代表什么:)
2021-02-21 21:09:23
而尝试使异步函数同步执行将尝试在 1 秒时喝掉咖啡,并在 1 分钟时将其倒入您的膝盖。
2021-02-26 21:09:23

其他答案非常好,我只想对此提供一个直接的答案。仅限于 jQuery 异步调用

所有 ajax 调用(包括$.getor$.post$.ajax)都是异步的。

考虑你的例子

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

代码执行从第 1 行开始,声明变量并在第 2 行(即 post 请求)上触发和异步调用,然后从第 3 行继续执行,无需等待 post 请求完成其执行。

假设 post 请求需要 10 秒才能完成, 的值outerScopeVar只会在这 10 秒后设置。

尝试,

var outerScopeVar; //line 1
$.post('loldog', function(response) {  //line 2, takes 10 seconds to complete
    outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun");  //line 3
alert(outerScopeVar);  //line 4

现在,当您执行此操作时,您将在第 3 行收到警报。现在等待一段时间,直到您确定发布请求已返回某个值。然后,当您单击“确定”时,在警报框中,下一个警报将打印预期值,因为您正在等待它。

在现实生活场景中,代码变成,

var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
    alert(outerScopeVar);
});

所有依赖于异步调用的代码都被移动到异步块内,或者等待异步调用。

你有一个快速的语法示例吗?
2021-02-14 21:09:23
or by waiting on the asynchronous calls 如何做到这一点?
2021-03-04 21:09:23
@InteXX 通过使用回调方法
2021-03-05 21:09:23