在函数内部修改变量后,为什么变量未更改?-异步代码参考


736

给定以下示例,为什么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找到这个问题。
法布里西奥磨砂

5
多加思考,标题中的“规范异步主题”有点繁琐,“异步代码参考”更简单,更客观。我也相信大多数人会搜索“异步”而不是“异步性”。
法布里西奥磨砂

2
有些人在函数调用之前初始化变量。如何更改以某种方式表示的标题?就像“为什么在函数内部修改变量后为什么变量不变?” ?
Felix Kling 2014年

在上面提到的所有代码示例中,“ alert(outerScopeVar);” 立即执行,而将值分配给“ outerScopeVar”则稍后(异步)进行。
重构

Answers:


580

一句话回答:异步性

前言

在此,至少在堆栈溢出中已经重复了数千次此主题。因此,首先,我想指出一些非常有用的资源:


眼前问题的答案

让我们首先跟踪常见行为。在所有示例中,outerScopeVar都在函数内部修改了。该函数显然不会立即执行,而是被分配或作为参数传递。这就是我们所说的回调

现在的问题是,何时调用该回调?

这要视情况而定。让我们尝试再次跟踪一些常见行为:

  • img.onload可能在将来(以及是否)成功加载图像时调用。
  • setTimeout在延迟到期并且超时尚未被取消后,可能在将来的某个时间调用clearTimeout。注意:即使0用作延迟,所有浏览器也具有最小超时延迟上限(在HTML5规范中指定为4毫秒)。
  • jQuery $.post的回调可能在将来的某个时间(如果(如果)成功完成Ajax请求)调用。
  • 当文件已成功读取或引发错误时,将来fs.readFile可能会调用Node.js。

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

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

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

也就是说,以手绘红色形状突出显示的异步代码只能在它们各自的代码块中的所有其余同步代码都已执行之后执行:

异步代码突出显示

简而言之,回调函数是同步创建的,但异步执行。在知道异步函数已执行之前,您就不能依靠它执行该如何执行?

真的很简单。应从该异步函数内部启动/调用依赖于异步函数执行的逻辑。例如,将alerts和console.logs 移到回调函数中也将输出预期的结果,因为此时该结果可用。

实现自己的回调逻辑

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

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

承诺

尽管可以通过香草JS 来阻止回调地狱,但Promise越来越流行,并且目前已在ES6中标准化(请参阅Promise-MDN)。

Promises(又名Futures)提供了一种更加线性,从而令人愉悦的异步代码阅读方式,但是解释其全部功能不在此问题范围之内。相反,我会将这些出色的资源留给感兴趣的人:


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


注意:我已将此答案标记为Community Wiki,因此具有至少100个信誉的任何人都可以对其进行编辑和改进!请随时改进此答案,或者也可以提交一个全新的答案。

我想将这个问题变成一个规范的主题,以回答与Ajax无关的异步性问题(为此,有一种如何从AJAX调用返回响应的方法),因此,这个主题需要您的帮助,以使其尽可能好和有用。 !


1
在您的最后一个示例中,是否有特定的原因为什么您要使用匿名函数,或者使用命名函数使其工作相同?
JDelage

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

2
僵局了吗?菲利克斯·克林(Felix kling)指向您的答案,而您指向菲利克斯(Felix)的答案
Mahi

1
您需要了解,红圈代码仅是异步的,因为它是由NATIVE异步javascript函数执行的。这是您的JavaScript引擎的功能-无论是Node.js还是浏览器。它是异步的,因为它作为“回调”传递给实质上是黑匣子的函数(在C等语言中实现)。对于不幸的开发人员来说,它们是不同步的……只是因为。如果要编写自己的异步函数,则必须通过将其发送到SetTimeout(myfunc,0)来对其进行破解。你应该那样做吗?另一场辩论……可能没有。
肖恩·安德森

@Fabricio我已经看过该规范定义“> = 4ms的钳”,但无法找到它-我在MDN发现了类似的机制,一些提(夹紧嵌套调用) - developer.mozilla.org/en-US/docs / Web / API /… -是否有人链接到HTML规范的右侧。
塞比

156

Fabrício的答案就在现场。但我想用一些不太技术的东西来补充他的答案,该技术着重于类比,以帮助解释异步性的概念


比喻...

昨天,我正在做的工作需要同事提供一些信息。我给他打电话。对话过程如下:

:嗨鲍勃,我需要知道我们如何 “d的酒吧 ”上周天。吉姆想要一份报告,而您是唯一知道此事细节的人。

鲍勃:好的,但是我要花30分钟左右吗?

:太好了,鲍勃。了解信息后给我回电话!

此时,我挂了电话。由于我需要鲍勃提供的信息来完成我的报告,因此我离开了报告,去喝咖啡,然后挂了一些电子邮件。40分钟后(鲍勃很慢),鲍勃回电话给了我我需要的信息。至此,我有了我需要的所有信息,就可以继续处理报告了。


想象一下,如果谈话像这样进行了;

:嗨鲍勃,我需要知道我们如何 “d的酒吧 ”上周天。吉姆想要一份关于它的报告,而您是唯一知道有关它的细节的报告。

鲍勃:好的,但是我要花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,这是一个很简单的操作。一旦完成此缓慢的操作,便会执行回调函数,但与此同时,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()返回undefinedundefined被警告。

为了解决这个问题,我们需要允许函数调用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)。


4
但是,如果要在其他计算中使用结果或将结果存储在对象变量中,警报或发送到控制台有什么用呢?
肯·英格拉姆

73

对于正在寻求快速参考的人们,以及使用promise和async / await的一些示例,这是一个更简洁的答案。

对于调用异步方法(在本例中为setTimeout)并返回一条消息的函数,从幼稚的方法(不起作用)开始:

function getMessage() {
  var outerScopeVar;
  setTimeout(function() {
    outerScopeVar = 'Hello asynchronous world!';
  }, 0);
  return outerScopeVar;
}
console.log(getMessage());

undefined在这种情况下被记录,因为getMessagesetTimeout调用回调和更新之前返回outerScopeVar

解决它的两种主要方法是使用回调Promise

回呼

此处的更改是getMessage接受一个callback参数,一旦可用,该参数将被调用以将结果传递回调用代码。

function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});

承诺

Promises提供了一种比回调更灵活的替代方法,因为它们可以自然地组合在一起以协调多个异步操作。甲承诺/ 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提供的功能类似于其Deferred的Promise。

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

异步/等待

如果您的JavaScript环境包含对async和的支持await(例如Node.js 7.6+),那么您可以在async函数内同步使用Promise :

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。为什么这不是其他任何地方都令人困惑。
文森特·P

一切都很好,但是如果您需要使用参数调用getMessage()怎么办?在这种情况下,您将如何编写以上内容?
Chiwda '17

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

我正在尝试您的async/await样本,但是遇到了问题。new Promise.Get()没有进行实例化,而是打电话,因此无法访问任何resolve()方法。因此,我getMessage()返回的是承诺,而不是结果。您可以稍微修改一下答案以显示适用的语法吗?
InteXX

@InteXX我不确定您.Get()打电话的意思。最好发布一个新问题。
JohnnyHK

56

显而易见,杯子代表outerScopeVar

异步功能就像...

异步调用咖啡


14
试图使异步功能同步起作用的方法是,尝试在1秒钟内喝咖啡,然后在1分钟内将其倒入您的腿上。
Teepeemm 2015年

如果说的是显而易见的话,我认为不会问这个问题,不是吗?
broccoli2000

2
@ broccoli2000我的意思不是说问题很明显,但是很明显杯子在图中代表了什么:)
Johannes Fahrenkrug

14

其他答案非常好,我只想对此提供一个简单的答案。仅限于jQuery异步调用

所有ajax调用(包括$.getor $.post或or $.ajax)都是异步的。

考虑你的例子

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

代码执行从第1行开始,在第2行声明变量并触发并进行异步调用(即发布请求),然后从第3行继续执行,而无需等待发布请求完成其执行。

可以说,发布请求需要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);
});

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


or by waiting on the asynchronous calls如何做到这一点?
InteXX

@InteXX通过使用回调方法
Teja,

您有一个简单的语法示例吗?
InteXX '19

11

在所有这些情况下,异步地outerScopeVar修改或为其分配一个值,或者在以后发生(等待或侦听某个事件发生)的情况下发生该值,当前执行将不会等待。因此,所有这些情况下,当前执行流程会导致outerScopeVar = undefined

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

1。

在此处输入图片说明

在这里,我们寄存器哪个将在image.Then的该特定event.Here加载当前执行与下一行连续执行的eventlistner img.src = 'lolcat.png';alert(outerScopeVar);同时可以不发生的事件。即,功能img.onload异步地等待引用的图像加载。这将在以下所有示例中发生-事件可能有所不同。

2。

2

此处,超时事件起着作用,它将在指定时间后调用处理程序。这里是0,但仍会注册一个异步事件,它将被添加到的最后位置Event Queue以执行,从而保证了延迟。

3。

在此处输入图片说明 这次是ajax回调。

4。

在此处输入图片说明

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

5,

在此处输入图片说明

明显的承诺(将来会做一些事情)是异步的。看到 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.