鉴于以下示例,为什么outerScopeVar在所有情况下都是未定义的?
outerScopeVar
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在所有这些示例中输出?我不想要解决方法,我想知道 为什么 会这样。
undefined
注意:这是一个关于 JavaScript 异步 性的规范问题。随意改进这个问题并添加更多社区可以识别的简化示例。
一句话回答: 异步 。
在 Stack Overflow 中,这个话题已经被重复了至少几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling 对“如何从异步调用返回响应?”的回答 . 请参阅他解释同步和异步流程的出色答案,以及“重构代码”部分。 @Benjamin Gruenbaum 在同一个线程中也付出了很多努力来解释异步性。
@Matt Esch 对“从 fs.readFile 获取数据”的回答也以简单的方式很好地解释了异步性。
让我们首先追踪常见的行为。在所有示例中,都在 函数outerScopeVar内部进行了修改。该函数显然不会立即执行,它被分配或作为参数传递。这就是我们所说的 回调 。 __ __
现在的问题是,何时调用该回调?
这取决于具体情况。让我们再次尝试追踪一些常见的行为:
img.onload
setTimeout
clearTimeout
0
$.post
fs.readFile
在所有情况下,我们都有一个可能 在未来某个时间 运行的回调。这种“未来某个时候”就是我们所说的 异步流 。
异步执行被推出同步流。也就是说,异步代码 永远不会 在同步代码堆栈执行时执行。这就是 JavaScript 单线程的意义。
更具体地说,当 JS 引擎空闲时——不执行(a)同步代码堆栈——它将轮询可能触发异步回调的事件(例如过期超时、接收到网络响应)并一个接一个地执行它们。这被视为事件循环。
也就是说,手绘红色形状中突出显示的异步代码只有在其各自代码块中的所有剩余同步代码都已执行后才能执行:
简而言之,回调函数是同步创建但异步执行的。在您知道异步函数已经执行之前,您不能依赖它的执行,以及如何做到这一点?
这很简单,真的。应该从这个异步函数内部启动/调用依赖于异步函数执行的逻辑。例如,在回调函数中移动alerts 和console.logs 会输出预期的结果,因为此时结果是可用的。
alert
console.log
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们处理一个更复杂的例子:
var outerScopeVar; helloCatAsync(); alert(outerScopeVar); function helloCatAsync() { setTimeout(function() { outerScopeVar = 'Nya'; }, Math.random() * 2000); }
注意: 我使用setTimeout随机延迟作为通用异步函数,同样的示例适用于 Ajax readFile、onload和任何其他异步流程。
readFile
onload
这个例子显然和其他例子有同样的问题,它没有等到异步函数执行。
让我们通过实现我们自己的回调系统来解决它。首先,我们摆脱了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本演示示例中的实现)。您只需要传递回调函数并了解它将在同步流之外执行,然后重新构建代码以适应它。
helloCatAsync
您还会注意到,由于异步性质,不可能将return异步流中的值返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后才执行的。
return
return您必须使用回调模式,或者...... Promises,而不是从异步回调中获取值。
尽管有一些方法可以使用 vanilla JS 来阻止回调地狱,但 Promise 越来越受欢迎,并且目前正在 ES6 中标准化(请参阅Promise - MDN)。
Promises(又名 Futures)提供了对异步代码的更线性,因此更愉快的阅读,但解释它们的整个功能超出了这个问题的范围。相反,我会将这些优秀的资源留给感兴趣的人:
注意: 我已将此答案标记为社区 Wiki,因此任何拥有至少 100 名声望的人都可以编辑和改进它!请随时改进此答案,或者如果您愿意,也可以提交一个全新的答案。 我想把这个问题变成一个规范的话题来回答与 Ajax 无关的异步问题(有How to return the response from an AJAX call? for that),因此这个话题需要你的帮助才能尽可能好和有帮助!
注意: 我已将此答案标记为社区 Wiki,因此任何拥有至少 100 名声望的人都可以编辑和改进它!请随时改进此答案,或者如果您愿意,也可以提交一个全新的答案。
我想把这个问题变成一个规范的话题来回答与 Ajax 无关的异步问题(有How to return the response from an AJAX call? for that),因此这个话题需要你的帮助才能尽可能好和有帮助!