鉴于以下示例,为什么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
?我不想要解决方法,我想知道为什么这种情况正在发生。
注意:这是 JavaScript asynchronicity 的规范性问题。随意改进这个问题,并添加社区可以识别的更简化的例子。
答案 0 :(得分:519)
一个字回答: asynchronicity 。
此主题至少已在Stack Overflow中迭代了几千次。因此,首先,我想指出一些非常有用的资源:
@Felix Kling's "How to return the response from an AJAX call"。请参阅他的优秀答案,解释同步和异步流程,以及"重组代码"节。
@Benjamin Gruenbaum也在同一个帖子中付出了很多努力来解释异步性。
@Matt Esch's answer to "Get data from fs.readFile"也以简单的方式非常好地解释了异步性。
让我们首先追踪共同的行为。在所有示例中,outerScopeVar
都在函数中修改。该功能显然不会立即执行,而是作为参数分配或传递。这就是我们所说的 回调 。
现在的问题是,该回调何时被称为?
这取决于具体情况。让我们再尝试追踪一些常见的行为:
img.onload
可能在将来某个时间被称为 ,当图像成功加载时(以及如果)。setTimeout
未取消超时后,clearTimeout
可能在将来某个时间被称为。注意:即使使用0
作为延迟,所有浏览器都有最小超时延迟上限(在HTML5规范中指定为4ms)。$.post
的回调可能在将来某个时间被称为 ,当Ajax请求成功完成时(以及如果)。fs.readFile
可能在将来的某个时间被称为 。在所有情况下,我们都有一个回调,可能会在将来的某个时间运行。这个"将来的某个时间"我们称之为异步流。
异步执行被推出同步流。也就是说,异步代码将在执行同步代码堆栈时永远执行。这就是JavaScript是单线程的意思。
更具体地说,当JS引擎空闲时 - 不执行(a)同步代码的堆栈 - 它将轮询可能触发异步回调的事件(例如,过期超时,接收到的网络响应)并执行它们接连不断。这被视为Event Loop。
也就是说,手绘红色形状中突出显示的异步代码只有在各个代码块中的所有剩余同步代码执行完后才能执行:
简而言之,回调函数是同步创建的,但是异步执行。你不能依赖异步函数的执行,直到你知道它已执行,以及如何做到这一点?
这很简单,真的。应该从这个异步函数内部启动/调用依赖于异步函数执行的逻辑。例如,在回调函数中移动alert
和console.log
也会输出预期结果,因为结果在此时可用。
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们来解决一个更复杂的例子:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
注意:我使用带有随机延迟的setTimeout
作为通用异步函数,同样的示例适用于Ajax,readFile
,{{1}和任何其他异步流程。
这个例子显然遇到与其他例子相同的问题,它不会等到异步函数执行。
让我们解决它实现我们自己的回调系统。首先,我们摆脱了丑陋的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);
}

大多数情况下,在实际使用案例中,DOM API和大多数库已经提供了回调功能(本演示示例中的// 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);
}
实现)。您只需要传递回调函数并了解它将在同步流程之外执行,并重新构建代码以适应这一点。
您还会注意到,由于异步性质,不可能helloCatAsync
从异步流返回到定义回调的同步流中的值,因为异步回调在同步后很久执行代码已经完成执行。
不是return
来自异步回调的值,而是必须使用回调模式,或者...... Promises。
尽管有很多方法可以使callback hell与香草JS保持一致,但承诺越来越受欢迎,目前正在ES6中进行标准化(见Promise - MDN)。
Promises(a.k.a.Fortures)提供了一个更线性,更令人愉快的异步代码读取,但解释它们的整个功能超出了这个问题的范围。相反,我会为感兴趣的人留下这些优秀的资源:
注意:我已将此答案标记为社区Wiki,因此任何拥有至少100个声誉的人都可以编辑和改进它!请随时改进这个答案,或者如果您也愿意,也可以提交一个全新的答案。
我想把这个问题变成一个规范的主题来回答与Ajax无关的异步性问题(对此有How to return the response from an AJAX call?),因此这个主题需要你的帮助尽可能好和有用! / p>
答案 1 :(得分:142)
Fabrício的回答是正确的;但是我想用一些不太技术性的东西来补充他的答案,这些东西专注于一个类比来帮助解释异步性的概念。
昨天,我所做的工作需要一位同事的一些信息。我打电话给他;这是对话的方式:
我:嗨鲍勃,我需要知道上周我们 foo ' bar 的方式。吉姆想要一份关于它的报告,而你是唯一了解它的细节的人。
Bob :当然可以,但是我需要大约30分钟?
我:那是伟大的鲍勃。当你得到这些信息时,给我一个回铃!
此时,我挂了电话。由于我需要鲍勃提供的信息来完成我的报告,我离开了报告并转而喝咖啡,然后我发现了一些电子邮件。 40分钟后(鲍勃很慢),鲍勃打电话给我,告诉我我需要的信息。此时,我继续使用我的报告工作,因为我掌握了所需的所有信息。
想象一下,如果谈话已经改变了,那么
我:嗨鲍勃,我需要知道上周我们 foo ' bar 的方式。吉姆想要一个关于它的报告,而你是唯一知道它的细节的人。
Bob :当然可以,但是我需要大约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
,这是 sloooow 操作。这个慢速操作完成后将执行回调函数,但与此同时,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()
is evil用于此目的
您目前可能有类似的代码;
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
)。
答案 2 :(得分:67)
对于正在寻找快速参考的人以及使用promises和async / await的一些示例,这里有一个更简洁的答案。
从调用异步方法的函数(在这种情况下为setTimeout
)开始,使用简单的方法(不起作用)并返回一条消息:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
在这种情况下会记录 undefined
因为getMessage
在调用setTimeout
回调之前返回并更新outerScopeVar
。
解决问题的两种主要方法是使用回调和承诺:
<强>回调强>
此处的更改是getMessage
接受callback
参数,该参数将被调用以在结果可用时将结果传回给调用代码。
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
<强> Promises 强>
Promise提供了一种比回调更灵活的替代方案,因为它们可以自然地组合起来协调多个异步操作。 node {js(0.12 +)和许多当前浏览器本身提供了Promises/A+标准实现,但也在Bluebird和Q等库中实现。
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Deferreds
jQuery提供的功能类似于Deferreds的承诺。
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
<强>异步/ AWAIT 强>
如果您的JavaScript环境包含对async
和await
的支持(如Node.js 7.6+),那么您可以在async
函数中同步使用promises:
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();
答案 3 :(得分:51)
答案 4 :(得分:13)
其他答案非常好,我只想提供一个直截了当的答案。仅限于jQuery异步调用
所有ajax调用(包括$.get
或$.post
或$.ajax
)都是异步的。
考虑你的例子
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
代码执行从第1行开始,在第2行声明变量和触发器以及异步调用(即post请求),并从第3行继续执行,而不等待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);
});
依赖于异步调用的所有代码都在异步块内移动,或者等待异步调用。
答案 5 :(得分:10)
在所有这些情况下,outerScopeVar
被修改或分配值异步或稍后发生(等待或侦听某些事件发生),当前执行不会等待。因此,所有这些情况下,当前执行流程都会导致outerScopeVar = undefined
让我们讨论每个例子(我标记了某些事件发生时被异步或延迟调用的部分):
<强> 1 强>
这里我们注册一个eventlistner,它将在该特定事件上执行。加载图像。然后当前执行与下一行img.src = 'lolcat.png';
和alert(outerScopeVar);
连续,同时事件可能不会发生。函数img.onload
等待所引用的图像加载,异步。这将发生在以下所有示例中 - 事件可能不同。
<强> 2 强>
此处超时事件扮演角色,该角色将在指定时间后调用处理程序。这里是0
,但它仍会注册一个异步事件,它将被添加到Event Queue
的最后一个位置以便执行,从而保证延迟。
第3 强>
<强> 4 强>
节点可以被认为是异步编码之王。这里标记的函数被注册为一个回调处理程序,它将在读取指定的文件后执行。
<强> 5 强>
明显的承诺(将来会做的事情)是异步的。见What are the differences between Deferred, Promise and Future in JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript