对于下面的例子,为什么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);

为什么在所有这些例子中输出都是未定义的?我不想要变通办法,我想知道为什么会这样。


注意:这是JavaScript异步性的典型问题。请随意改进这个问题,并添加更多社区可以认同的简化示例。


当前回答

在所有这些场景中,outerScopeVar都是异步修改或分配值的,或者在稍后发生(等待或监听某些事件的发生),当前执行将不会等待这些事件。所有这些情况下,当前执行流的结果都是outerScopeVar = undefined

让我们来讨论每个例子(我标记了被称为异步或延迟发生某些事件的部分):

1.

这里我们注册了一个eventlistner,它将在特定事件时执行。这里加载图像。然后,当前执行继续与下一行img。SRC = 'lolcat.png';和警报(outerScopeVar);与此同时,事件可能不会发生。也就是函数img。Onload等待被引用的图像异步加载。这将发生在所有下面的例子-事件可能不同。

2.

在这里,超时事件起作用,它将在指定的时间之后调用处理程序。这里它是0,但它仍然注册了一个异步事件,它将被添加到事件队列的最后一个位置执行,这使得有保证的延迟。

3.

这次是ajax回调。

4.

节点可以被认为是异步编码的王者。这里标记的函数被注册为回调处理程序,该处理程序将在读取指定文件后执行。

5.

显而易见的承诺(将来会做某事)是异步的。JavaScript中Deferred, Promise和Future的区别是什么?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

其他回答

其他的答案都很好,我只是想提供一个直接的答案。仅限于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请求完成执行。

假设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行得到一个警告。现在等待一段时间,直到您确定post请求已经返回了一些值。然后,当您单击警报框上的OK时,下一个警报将打印预期值,因为您一直在等待它。

在现实生活中,代码会变成,

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

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

答案只有一个字:异步性。

前言

这个主题在Stack Overflow中已经被迭代了至少几千次。因此,首先我想指出一些非常有用的资源:

@Felix Kling对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流的精彩回答,以及“重构代码”一节。 @Benjamin Gruenbaum也花了很多精力来解释同一个线程中的异步性。 @Matt Esch对“从fs获取数据”的回答。readFile也以简单的方式很好地解释了异步性。


手头问题的答案

让我们先追溯一下常见的行为。在所有示例中,outerScopeVar都是在函数内部修改的。该函数显然没有立即执行;它被赋值或作为参数传递。这就是我们所说的回调。

问题是,这个回调什么时候被调用?

这要看情况而定。让我们再试着追踪一些常见的行为:

img.onload may be called sometime in the future when (and if) the image has successfully loaded. setTimeout may be called sometime in the future after the delay has expired and the timeout hasn't been canceled by clearTimeout. Note: even when using 0 as delay, all browsers have a minimum timeout delay cap (specified to be 4ms in the HTML5 spec). jQuery $.post's callback may be called sometime in the future when (and if) the Ajax request has been completed successfully. Node.js's fs.readFile may be called sometime in the future when the file has been read successfully or thrown an error.

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

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

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

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

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

这真的很简单。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,移动警报和控制台。回调函数内的日志将输出预期的结果,因为结果在那时是可用的。

实现自己的回调逻辑

通常,您需要对异步函数的结果做更多的事情,或者根据异步函数被调用的位置对结果做不同的事情。让我们来看一个更复杂的例子:

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实现)。您只需要传递回调函数,并理解它将在同步流之外执行,并重新构造代码以适应这一点。

您还将注意到,由于异步特性,不可能将一个值从异步流返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后才执行的。

您将不得不使用回调模式,而不是从异步回调返回值,或者……承诺。

承诺

虽然在普通JS中有一些方法可以避免回调的麻烦,但是承诺越来越受欢迎,目前正在ES6中标准化(参见承诺- MDN)。

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

JavaScript承诺- HTML5岩石 你错过了承诺的意义- domenic.me


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

Node的艺术-回调用普通JS示例和Node. JS代码很好地解释了异步代码和回调。


注意:我已经把这个答案标记为社区维基。因此,任何拥有至少100个声誉的人都可以编辑和改进它!如果您愿意,请随时改进这个答案或提交一个全新的答案。 我想把这个问题变成一个规范的主题,以回答与Ajax无关的异步性问题(如何从Ajax调用返回响应?为此),因此这个话题需要你的帮助,尽可能地好和有用!

Fabrício的答案是正确的;但我想用一些不那么技术性的东西来补充他的回答,侧重于一个类比来帮助解释异步性的概念。


一个类比……

昨天,我正在做的工作需要从一位同事那里得到一些信息。我给他打了电话;对话是这样进行的:

我:嗨,鲍勃,我想知道上周我们是怎么在酒吧吃饭的。吉姆想要一份关于它的报告,而你是唯一知道细节的人。

当然可以,但是大概要30分钟吧?

我:太好了,鲍勃。你得到消息后给我回个电话!

这时,我挂了电话。因为我需要鲍勃提供一些信息来完成我的报告,所以我放下报告去喝了杯咖啡,然后又处理了一些电子邮件。40分钟后(Bob反应慢),Bob给我回了电话,告诉了我所需要的信息。在这一点上,我继续我的工作和我的报告,因为我有所有我需要的信息。


想象一下,如果谈话是这样进行的;

我:嗨,鲍勃,我想知道上周我们是怎么在酒吧吃饭的。吉姆想要一份关于它的报告,而你是唯一知道细节的人。

当然可以,但是大概要30分钟吧?

我:太好了,鲍勃。我将等待。

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


这是异步与同步行为

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

Rather than waiting for these slow operations to complete, JavaScript lets you register a callback function which will be executed when the slow operation has completed. In the meantime, however, JavaScript will continue to execute other code. The fact that JavaScript executes other code whilst waiting for the slow operation to complete makes the behaviorasynchronous. Had JavaScript waited around for the operation to complete before executing any other code, this would have been synchronous behavior.

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将继续处理下一行代码;即警报(outerScopeVar)。

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

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

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

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

img.src = 'lolcat.png';

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

因此,在我们所有的例子中,函数(){/* 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);

然而,我们现在知道返回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)。

在所有这些场景中,outerScopeVar都是异步修改或分配值的,或者在稍后发生(等待或监听某些事件的发生),当前执行将不会等待这些事件。所有这些情况下,当前执行流的结果都是outerScopeVar = undefined

让我们来讨论每个例子(我标记了被称为异步或延迟发生某些事件的部分):

1.

这里我们注册了一个eventlistner,它将在特定事件时执行。这里加载图像。然后,当前执行继续与下一行img。SRC = 'lolcat.png';和警报(outerScopeVar);与此同时,事件可能不会发生。也就是函数img。Onload等待被引用的图像异步加载。这将发生在所有下面的例子-事件可能不同。

2.

在这里,超时事件起作用,它将在指定的时间之后调用处理程序。这里它是0,但它仍然注册了一个异步事件,它将被添加到事件队列的最后一个位置执行,这使得有保证的延迟。

3.

这次是ajax回调。

4.

节点可以被认为是异步编码的王者。这里标记的函数被注册为回调处理程序,该处理程序将在读取指定文件后执行。

5.

显而易见的承诺(将来会做某事)是异步的。JavaScript中Deferred, Promise和Future的区别是什么?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

显而易见,杯子代表outerScopeVar。

异步函数就像……