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


605

鉴于以下示例,为什么 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异步性 。随意改进这个问题,并添加社区可以识别的更简化的示例。



@Dukeling谢谢,我很确定我已经对该链接进行了评论,但显然有一些缺失的评论。此外,关于你的编辑:我相信标题中的“规范”和“异步性”有助于在搜索此问题时将另一个问题标记为欺骗。当然,在寻找异步性解释时,它也有助于从Google中找到这个问题。
Fabrício Matté

3
更多的想法,“规范的异步性主题”在标题上有点沉重,“异步代码引用”更简单,更客观。我也相信大多数人都在寻找“异步”而不是“异步性”。
Fabrício Matté

1
有些人在函数调用之前初始化它们的变量。如何更改以某种方式表示的标题呢?喜欢“为什么我的变量 不变 在我修改函数内部后?“?
Felix Kling

在上面提到的所有代码示例中,“alert(outerScopeVar);”现在执行,而为“outerScopeVar”赋值会发生一次(异步)。
refactor

Answers:


493

一个字回答: 异步

前言

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


手头问题的答案

让我们首先追踪共同的行为。在所有的例子中, outerScopeVar 在...内修改 功能 。该功能显然不会立即执行,而是作为参数分配或传递。这就是我们所说的 打回来

现在的问题是,该回调何时被称为?

这取决于具体情况。让我们再次尝试追踪一些常见的行为:

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

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

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

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

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

async code highlighted

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

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

实现自己的回调逻辑

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

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 从异步回调中获取值,您将不得不使用回调模式,或者...... Promises。

承诺

虽然有办法保持 回调地狱 与香草JS一起,承诺越来越受欢迎,目前正在ES6中进行标准化(参见 承诺 - MDN )。

Promises(a.k.a.Fortures)提供了一个更线性,更令人愉快的异步代码读取,但解释它们的整个功能超出了这个问题的范围。相反,我将为感兴趣的人留下这些优秀的资源:


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


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

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


1
在上一个示例中,是否存在使用匿名函数的特定原因,或者使用命名函数是否可以使用匿名函数?
JDelage

1
代码示例有点奇怪,因为您在调用它之后声明了该函数。当然因为吊装而起作用,但这是故意的吗?
Bergi

2
这是僵局吗? felix kling指向你的答案,你指的是felix的回答
Mahi

1
您需要了解红圈代码只是异步,因为它是由NATIVE异步JavaScript函数执行的。这是您的javascript引擎的一个功能 - 无论是Node.js还是浏览器。它是异步的,因为它作为“回调”传递给一个本质上是黑盒子的函数(在C等中实现)。对于倒霉的开发者来说,他们是异步的...只是因为。如果你想编写自己的异步函数,你必须通过将其发送到SetTimeout(myfunc,0)来破解它。你应该这样做吗?另一场辩论......可能不是。
Sean Anderson

2
漂亮的写意圈
Patrick Roberts

138

Fabrício的回答是现场的;但是我想用一些不太技术性的东西来补充他的答案,这些东西专注于类比以帮助解释异步性的概念


比喻......

昨天,我所做的工作需要一位同事的一些信息。我打电话给他;这是对话的方式:

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

短发 :当然可以,但是我需要30分钟左右?

:那是伟大的鲍勃。当你掌握了这些信息后,给我一个回铃!

此时,我挂了电话。由于我需要鲍勃提供的信息来完成我的报告,我离开了报告并转而喝咖啡,然后我发现了一些电子邮件。 40分钟后(鲍勃很慢),鲍勃打电话给我,告诉我我需要的信息。此时,我继续使用我的报告工作,因为我拥有了我需要的所有信息。


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

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

短发 :当然可以,但是我需要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() 是邪恶的 以此目的


如何让我的来电者等待?

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

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 )。


7
该死的鲍勃,下次准时!无论如何好比喻!
Jimmy Knoot

很好的比喻和解释
Anupam

Upvoted是因为它帮助我从非编程背景中理解异步和同步。谢谢。
Mindthetic

真棒的比喻,我会在向别人解释时提醒一下! :)
Filipe Oliveira

64

对于正在寻找快速参考的人以及使用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);
});

承诺

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的 Deferreds

jQuery提供的功能类似于Deferreds的promises。

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环境包含支持 asyncawait (比如Node.js 7.6+),那么你可以在其中同步使用promises async 功能:

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();

你在Promises上的样本基本上就是我过去几个小时里一直在寻找的。你的榜样很漂亮,同时解释了Promises。为什么这不是其他任何地方都令人难以置信。
Vincent P

这一切都很好,但是如果你需要用参数调用getMessage()呢?在那种情况下你会怎么写上面的?
Chiwda

2
@Chiwda你只需将回调参数放在最后: function getMessage(param1, param2, callback) {...}
JohnnyHK

51

为了说清楚,杯子代表 outerScopeVar

异步函数就像......

asynchronous call for coffee


11
尽管试图使异步功能同步起作用,但试图在1秒内喝咖啡,然后在1分钟内倒入你的膝盖。
Teepeemm

如果它说明显了,我不认为会问这个问题,不是吗?
broccoli2000

2
@ broccoli2000由此我并不是说这个问题很明显,但很明显杯子在图纸中代表什么:)
Johannes Fahrenkrug

好照片:)哈哈
V. Kalyuzhnyu

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请求完成其执行。

让我们说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);
});

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


10

在所有这些情况下 outerScopeVar 被修改或赋值 异步 要么 发生在稍后的时间(等待或侦听某些事件发生),当前执行不会等待 因此,所有这些情况下的当前执行流程都会产生 outerScopeVar = undefined

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

1。

enter image description here

这里我们注册一个eventlistner,它将在特定事件上执行。加载图像。然后当前执行连续下一行 img.src = 'lolcat.png';alert(outerScopeVar); 同时事件可能不会发生。 i.f,功能 img.onload 等待引用的图像以异步方式加载。这将在以下所有示例中发生 - 事件可能不同。

2。

2

这里,timeout事件扮演角色,它将在指定时间后调用处理程序。这里是 0,但它仍然注册一个异步事件,它将被添加到的最后位置 Event Queue 执行,这使得保证延迟。

3。

enter image description here 这次是ajax回调。

4。

enter image description here

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

5。

enter image description here

明显的承诺(将来会做的事情)是异步的。看到 JavaScript中Deferred,Promise和Future之间有什么区别?

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.