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

Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference

提问人:Fabrício Matté 提问时间:5/15/2014 最后编辑:Christian Vincenzo TrainaFabrício Matté 更新时间:12/25/2022 访问量:299304

问:

鉴于以下示例,为什么在所有情况下都是未定义的?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 异步性的规范问题。随意改进这个问题,并添加社区可以识别的更简化的例子。

JavaScript 异步

评论

11赞 Bernhard Barker 5/16/2014
相关元讨论 - 用于接近投票的一般 JavaScript 异步参考?
0赞 Fabrício Matté 5/16/2014
@Dukeling谢谢,我很确定我已经用该链接发表了评论,但显然有一些遗漏的评论。另外,关于您的编辑:我相信在标题中使用“规范”和“异步性”有助于搜索此问题以将另一个问题标记为欺骗。当然,在寻找异步性解释时,它也有助于从 Google 找到这个问题。
8赞 Fabrício Matté 5/20/2014
多想一点,“规范异步主题”的标题有点重,“异步代码引用”更简单、更客观。我也相信大多数人搜索的是“异步”而不是“异步”。
4赞 Felix Kling 7/7/2014
有些人在函数调用之前初始化他们的变量。如何更改以某种方式代表这一点的标题?比如“为什么我的变量在函数内部修改后没有改变
1赞 refactor 5/3/2016
在上面提到的所有代码示例中,“alert(outerScopeVar);” 执行 NOW ,而将值赋给 “outerScopeVar” 发生在 LATER(异步)中。

答:

718赞 11 revs, 9 users 78%Fabrício Matté #1

一个字的答案:异步性

前言

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


手头问题的答案

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

现在的问题是,什么时候调用该回调?

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

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

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

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

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

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

async code highlighted

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

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

实现自己的回调逻辑

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

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

注意:我使用随机延迟作为通用异步函数;相同的示例适用于 Ajax、、 和任何其他异步流。setTimeoutreadFileonload

这个例子显然遇到了与其他例子相同的问题;它不会等到异步函数执行。

让我们通过实现我们自己的回调系统来解决它。首先,我们摆脱了在这种情况下完全无用的丑陋。然后我们添加一个接受函数参数的参数,即我们的回调。当异步操作完成后,我们调用此回调,传递结果。实现(请按顺序阅读评论):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

承诺

尽管有一些方法可以用普通的 JS 来避免回调的地狱,但 promise 越来越受欢迎,目前正在 ES6 中标准化(参见 Promise - MDN)。

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


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


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

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

评论

1赞 JDelage 5/4/2016
在上一个示例中,使用匿名函数是否有特定原因,或者使用命名函数会以相同的方式工作吗?
1赞 Bergi 5/6/2016
代码示例有点奇怪,因为您在调用函数后声明该函数。当然,因为吊装而起作用,但这是故意的吗?
3赞 Mahi 1/2/2017
是死锁吗.菲利克斯·克林(Felix Kling)指向您的答案,而您则指向菲利克斯(Felix)的答案
2赞 Sean Anderson 1/6/2017
您需要了解红色圆圈代码只是异步的,因为它是由 NATIVE 异步 javascript 函数执行的。这是 javascript 引擎的一项功能——无论是 Node.js 还是浏览器。它是异步的,因为它作为“回调”传入到本质上是一个黑盒(用 C 等实现)的函数。对于倒霉的开发人员来说,他们是异步的......只是因为。如果你想编写自己的异步函数,你必须通过将其发送到 SetTimeout(myfunc,0) 来破解它。你应该这样做吗?另一场辩论......可能不是。
0赞 The_Redhawk 3/20/2022
如果被调用的函数位于单独的文件中,这将如何工作?
191赞 15 revs, 7 users 94%Matt #2

法布里西奥的回答很到位,但我想用一些不那么技术性的东西来补充他的回答,它侧重于类比来帮助解释异步性的概念


打个比方......

昨天,我正在做的工作需要一位同事提供一些信息。我给他打了电话;谈话是这样的:

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

鲍勃:当然可以,但是我需要大约30分钟?

:那太好了,鲍勃。当你得到信息时,给我一个电话!

这时,我挂断了电话。由于我需要 Bob 提供的信息来完成我的报告,所以我离开了报告,转而去喝咖啡,然后我赶上了一些电子邮件。40 分钟后(Bob 很慢),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 加载 ,这是一个懒惰的操作。一旦这个缓慢的操作完成,回调函数就会被执行,但与此同时,JavaScript 将继续处理下一行代码;即.lolcat.pngalert(outerScopeVar)

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

为了修复我们的代码,我们所要做的就是将代码移动到回调函数中。因此,我们不再需要将变量声明为全局变量。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 outerScopeVaronloadgetWidthOfImage()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

评论

12赞 Ken Ingram 3/23/2019
但是,如果要在不同的计算中使用结果或将其存储在对象变量中,则警报或发送到控制台有何用处?
3赞 MasterMind 5/31/2021
这是整个 stackoverflow 上最有用、最快速、最简洁的答案。谢谢。
0赞 Timo 3/6/2022
最后一个代码示例显示了另一个参数(此处为 src)的用法,该参数与回调函数无关,甚至在回调之后插入到代码中。正如 Mastermind 所写的那样,简明扼要!
0赞 Timo 3/6/2022
我怎么知道它是异步的,当我也许可以将其重写为不可能但似乎是同步的。在 JS 中感觉在读了很多这些狗屎之后应该得到吗?img.onload=function(){..function img.onload(){..
95赞 JohnnyHK 1/21/2015 #3

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

从调用异步方法(在本例中)并返回消息的函数的朴素方法(不起作用)开始:setTimeout

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

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

解决此问题的两种主要方法是使用回调承诺

回调

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

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

承诺

Promise 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合起来以协调多个异步操作。Promises/A+ 标准实现在 node.js (0.12+) 和许多当前浏览器中原生提供,但也在 BluebirdQ 等库中实现。

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

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

jQuery 延迟

jQuery 提供的功能类似于 promise 及其 Deferreds。

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

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

异步/等待

如果您的 JavaScript 环境包括对 asyncawait 的支持(如 Node.js 7.6+),那么您可以在函数中同步使用 promise: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();

评论

4赞 Vincent P 1/8/2016
你关于Promises的样本基本上是我在过去几个小时里一直在寻找的。你的例子很漂亮,同时解释了 Promises。为什么这在其他任何地方都不是,这令人难以置信。
0赞 Chiwda 8/2/2017
这一切都很好,但是如果您需要使用参数调用 getMessage() 怎么办?在这种情况下,你会如何写上述内容?
2赞 JohnnyHK 8/2/2017
@Chiwda 您只需将回调参数放在最后: .function getMessage(param1, param2, callback) {...}
0赞 Daggerpov 5/31/2023
$.您提供的 Deferred() 实现正是我在 jQuery 中寻找的,因为我正在运行旧包,谢谢。
12赞 Tom Sebastian 10/27/2015 #4

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

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

1.

enter image description here

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

2.

2

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

3.

enter image description here这次是ajax回调。

4.

enter image description here

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

5.

enter image description here

显而易见的承诺(将来会做一些事情)是异步的。请参阅 JavaScript 中的 Deferred、Promise 和 Future 之间有什么区别?

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

61赞 Johannes Fahrenkrug 12/9/2015 #5

显而易见的是,杯子代表 .outerScopeVar

异步函数就像...

asynchronous call for coffee

评论

18赞 Teepeemm 12/10/2015
而尝试使异步函数同步操作将尝试在 1 秒内喝咖啡,并在 1 分钟内将其倒入您的膝盖。
0赞 broccoli2000 7/3/2016
如果它陈述是显而易见的,我认为不会有人问这个问题,不是吗?
2赞 Johannes Fahrenkrug 9/30/2016
@broccoli2000 我并不是说这个问题是显而易见的,而是说杯子在图画中代表什么是显而易见的:)
0赞 JoePythonKing 1/10/2022
如果杯子是变量,那么函数在哪里?
16赞 Teja 2/26/2016 #6

其他答案都很好,我只想提供一个直截了当的答案。仅限制为 jQuery 异步调用

所有 ajax 调用(包括 or 或 )都是异步的。$.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 秒才能完成,只有在这 10 秒之后才会设置 的值。outerScopeVar

要尝试,

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

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

0赞 samnoon 7/14/2022 #7

简短的回答是:异步性。

为什么需要异步?

JavaScript 是单线程的,这意味着脚本的两个位不能同时运行;他们必须一个接一个地奔跑。在浏览器中,JavaScript 与因浏览器而异的其他内容共享一个线程。但通常情况下,JavaScript 与绘制、更新样式和处理用户操作(例如突出显示文本和与表单控件交互)位于同一队列中。其中一件事情的活动会延迟其他事情。

您可能已经使用事件和回调来解决这个问题。以下是活动:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // image loaded
  console.log("Loaded");
});

img1.addEventListener('error', function() {
  // error caught
  console.log("Error printed");
});
<img class="img-1" src="#" alt="img">

这根本不是打喷嚏。我们得到图像,添加几个侦听器,然后 JavaScript 可以停止执行,直到调用其中一个侦听器。

不幸的是,在上面的示例中,这些事件可能在我们开始监听它们之前就发生了,因此我们需要使用图像的“complete”属性来解决这个问题:

var img1 = document.querySelector('.img-1');

function loaded() {
  // image loaded
  console.log("Loaded");
}

if (img1.complete) {
  loaded();
} else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // error caught
  console.log("Error printed");
});
<img class="img-1" src="#" alt="img">

这不会捕获在我们有机会收听它们之前出错的图像;不幸的是,DOM 没有给我们一种方法来做到这一点。此外,这是加载一个图像。如果我们想知道一组图像何时加载,事情会变得更加复杂。

活动并不总是最好的方式

事件非常适合在同一对象上可能多次发生的事情,等等。对于这些事件,您并不真正关心在附加侦听器之前发生了什么。keyuptouchstart

正确执行此操作的两种主要方法:回调和承诺。

回调

回调是在其他函数的参数中传递的函数,此过程在 JavaScript 中有效,因为函数是对象,对象可以作为参数传递给函数。回调函数的基本结构如下所示:

function getMessage(callback) {
  callback();
}

function showMessage() {
  console.log("Hello world! I am a callback");
}
getMessage(showMessage);

承诺

尽管有一些方法可以用 vanilla JS 来阻止回调地狱,但 promise 越来越受欢迎,目前正在 ES6 中标准化(参见 Promise)。

promise 是一个占位符,表示异步操作的最终结果(值)

  • promise 占位符将替换为结果值(如果成功)或失败原因(如果不成功)

如果你不需要知道某件事何时发生,而只需要知道它是否发生,那么承诺就是你要找的。

promise 有点像事件侦听器,不同之处在于:

  • 一个承诺只能成功或失败一次
  • 承诺不能从失败切换到成功,反之亦然
  • 一旦你有了结果,承诺是不可变的
  • 如果 promise 成功或失败,并且您稍后添加了成功/失败回调,则将调用正确的回调
  • 事件是否发生在添加回调之前并不重要

注意:始终从 Promise 中的函数返回结果,否则后续函数将无法执行任何操作。

Promise 术语

承诺可以是:

  • 已履行:与承诺相关的操作成功
    • 异步操作已完成
    • 承诺是有价值的
    • 承诺不会再改变
  • rejected:与承诺相关的操作失败
    • 异步操作失败
    • 诺言永远不会兑现
    • Promise 具有指示操作失败原因的原因
    • 承诺不会再改变
  • 待处理:尚未发货或已拒绝
    • 异步操作尚未完成
    • 可以转换为“已发货”或“已拒绝”
  • settled:已履行或拒绝,因此是不可变的

如何创建承诺

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello world! I am a promise');
    }, 0);
  });
}

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