给出以下示例,为什么在所有情况下 outerScopeVar 都未定义?var outerScopeVar;var img = document.createElement('img');img.onload = function() { outerScopeVar = this.width;};...
给出以下例子,为什么 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);
// with observables
var outerScopeVar;
myObservable.subscribe(function (value) {
outerScopeVar = value;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
在所有这些示例中 undefined
都会输出 为什么 会发生这种情况。
注意: JavaScript 异步性 的典型问题 。欢迎改进此问题并添加更多社区可以识别的简化示例。
答案只有一个词: 异步性 .
这个话题在 Stack Overflow 上至少被重复了几千次。因此,首先我想指出一些非常有用的资源:
p2
p3
让我们首先追溯一下常见的行为。在所有示例中,都 outerScopeVar
函数 内部进行了修改 。该函数显然不会立即执行;它被赋值或作为参数传递。这就是我们所说的 回调 .
现在的问题是,什么时候调用该回调?
这要视情况而定。让我们再次尝试追踪一些常见行为:
img.onload
当图像成功加载时(如果成功加载), 在将来的某个时间 被调用
setTimeout
可能会 在延迟到期后某个时间 ,并且超时尚未被取消 clearTimeout
。注意:即使使用 0
延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4 毫秒)。
$.post
jQuery 的回调可能会 在将来的某个时间
fs.readFile
Node.js 可能会 在将来的某个时间
在未来某个时间 运行的回调 。这个“未来某个时间”就是我们所说的 异步流 .
异步执行被推出同步流程。也就是说,在同步代码堆栈执行时,异步代码永远不会 执行 。这就是 JavaScript 单线程的含义。
更具体地说,当 JS 引擎处于空闲状态(不执行(非)同步代码堆栈)时,它将轮询可能触发异步回调的事件(例如超时、收到网络响应)并一个接一个地执行它们。这被视为 事件循环 .
也就是说,手绘红色形状中突出显示的异步代码只有在其各自代码块中所有剩余的同步代码都执行完之后才会执行:
简而言之,回调函数是同步创建的,但异步执行的。您不能依赖异步函数的执行,除非您知道它已经执行,那么如何做到这一点?
其实很简单。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,在回调函数内部移动 s alert
和 console.log
s 将输出预期结果,因为此时结果可用。
通常,您需要使用异步函数的结果执行更多操作,或者根据异步函数的调用位置对结果执行不同的操作。让我们处理一个更复杂的例子:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
注意: 我使用 setTimeout
随机延迟作为通用异步函数;相同示例适用于 Ajax、 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 an 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 an 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 an 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
从异步回调中获取值,而必须使用回调模式,或者......承诺。
尽管有一些方法可以通过 vanilla JS 避免 回调地狱 ,但 Promise 越来越受欢迎,并且目前正在 ES6 中进行标准化(请参阅 Promise - MDN )。
Promises(又名 Futures)提供了更线性、更令人愉悦的异步代码阅读体验,但解释其全部功能超出了本文的范围。相反,我将为感兴趣的人留下这些优秀的资源:
注意: 我已将此答案标记为社区 Wiki。因此,任何拥有至少 100 个声誉的人都可以编辑和改进它!如果您愿意,请随意改进此答案或提交一个全新的答案。
我想将这个问题变成一个规范主题来回答与 Ajax 无关的异步问题(有关如何 从 AJAX 调用返回响应? ),因此这个主题需要您的帮助才能尽可能好和有用!