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

Posted

技术标签:

【中文标题】为啥我的变量在函数内部修改后没有改变? - 异步代码参考【英文标题】:Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference为什么我的变量在函数内部修改后没有改变? - 异步代码参考 【发布时间】:2022-01-19 20:40:48 【问题描述】:

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

【问题讨论】:

相关元讨论 - General JavaScript asynchronicity reference for close voting? @Dukeling 谢谢,我很确定我已经对该链接发表了评论,但显然有一些缺少的 cmets。另外,关于您的编辑:我相信在搜索此问题以将另一个问题标记为欺骗时,标题中包含“规范”和“异步”会有所帮助。当然,在寻找异步解释时,它也有助于从 Google 中找到这个问题。 再想一想,“canonical asynchricity topic”的标题有点重,“asynchronous code reference”更简单客观。我也相信大多数人会搜索“异步”而不是“异步”。 有些人在函数调用之前初始化他们的变量。如何更改以某种方式也代表这一点的标题?比如“为什么我的变量在函数内部修改后未改变?” ? 在您上面提到的所有代码示例中,“alert(outerScopeVar);”现在执行,而将值分配给“outerScopeVar”发生在以后(异步)。 【参考方案1】:

一句话回答:异步性

前言

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

@Felix Kling's answer to "How do I return the response from an asynchronous call?"。请参阅他解释同步和异步流程的出色答案,以及“重构代码”部分。 @Benjamin Gruenbaum 在同一个线程中也付出了很多努力来解释异步性。

@Matt Esch's answer to "Get data from fs.readFile" 也以简单的方式很好地解释了异步性。


手头问题的答案

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

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

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

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

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

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

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

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

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

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

实现自己的回调逻辑

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

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

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

注意:我使用带有随机延迟的setTimeout 作为通用异步函数,同样的示例适用于 Ajax、readFileonload 和任何其他异步流。

这个例子显然和其他例子有同样的问题,它没有等到异步函数执行。

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

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

上例代码sn-p:

// 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 回到定义回调的同步流,因为异步回调是在同步代码执行很久之后执行的已经执行完毕。

您必须使用回调模式,或者... Promise,而不是 return从异步回调中获取值。

承诺

尽管有一些方法可以让 callback hell 与 vanilla JS 保持距离,但 promise 越来越受欢迎,并且目前正在 ES6 中标准化(请参阅 Promise - MDN)。

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

JavaScript Promises - HTML5 Rocks You're Missing the Point of Promises - domenic.me

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

The Art of Node - Callbacks 用 vanilla JS 示例和 Node.js 代码很好地解释了异步代码和回调。

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

我想把这个问题变成一个规范的话题来回答与 Ajax 无关的异步问题(有How to return the response from an AJAX call? ),因此这个话题需要你的帮助才能尽可能好和有帮助!

【讨论】:

在你的最后一个例子中,你使用匿名函数是否有特定的原因,或者使用命名函数是否可以同样工作? 代码示例有点奇怪,因为您在调用函数后声明了该函数。当然是因为吊装才起作用,但这是故意的吗? 是死锁吗。 felix kling 指向你的答案,而你指向 felix 的答案 您需要了解红色圆圈代码只是异步的,因为它是由 NATIVE async javascript 函数执行的。这是您的 javascript 引擎的一个功能——无论是 Node.js 还是浏览器。它是异步的,因为它作为“回调”传递给本质上是一个黑盒(在 C 等中实现)的函数。对于倒霉的开发人员来说,它们是异步的……只是因为。如果你想编写自己的异步函数,你必须通过将它发送到 SetTimeout(myfunc,0) 来破解它。你应该这样做吗?另一场辩论......可能不会。 @Fabricio 我已经寻找定义“>=4ms 钳位”的规范,但找不到它 - 我在 MDN 上发现了一些类似机制(用于钳位嵌套调用)的提及 - developer.mozilla.org/en-US/docs/Web/API/… - 有没有人有指向 HTML 规范正确部分的链接。【参考方案2】:

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


类比...

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

:嗨,Bob,我想知道我们上周是如何foo搞定bar的。 Jim 想要一份关于它的报告,而你是唯一知道它的详细信息的人。

Bob:没问题,但我大概需要 30 分钟?

:太棒了,鲍勃。得到信息后给我回电话!

这时,我挂断了电话。因为我需要 Bob 提供的信息来完成我的报告,所以我离开了报告并去喝咖啡,然后我赶上了一些电子邮件。 40 分钟后(Bob 很慢),Bob 回电话给了我我需要的信息。至此,我恢复了我的报告工作,因为我拥有了我需要的所有信息。


想象一下,如果对话变成了这样;

:嗨,Bob,我想知道我们上周是如何foo搞定bar的。 Jim 想要一份关于它的报告,而你是唯一知道它的详细信息的人。

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 加载 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() is evil 用于此目的


如何让我的来电者等待?

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

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()的函数注册一个回调,然后将宽度的alert'ing移动到该回调内;

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

【讨论】:

但是,如果您想在不同的计算中使用结果,或者将其存储在对象变量中,那么警报或发送到控制台有何用处? 这是整个***上最有用、最快速、最简洁的答案。谢谢。【参考方案3】:

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

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

function getMessage() 
  var outerScopeVar;
  setTimeout(function() 
    outerScopeVar = 'Hello asynchronous world!';
  , 0);
  return outerScopeVar;

console.log(getMessage());

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

解决它的两种主要方法是使用 callbackspromises

回调

这里的变化是getMessage 接受callback 参数,一旦可用,将调用该参数以将结果返回给调用代码。

function getMessage(callback) 
  setTimeout(function() 
    callback('Hello asynchronous world!');
  , 0);

getMessage(function(message) 
  console.log(message);
);

Promises

Promises 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合以协调多个异步操作。 Promises/A+ 标准实现在 node.js (0.12+) 和许多当前浏览器中原生提供,但也在 Bluebird 和 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 提供类似于 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 环境支持 asyncawait(如 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。为什么这不是其他任何地方令人难以置信。 这一切都很好,但是如果您需要使用参数调用 getMessage() 怎么办?在那种情况下你会如何写上面的内容? @Chiwda 你把回调参数放在最后:function getMessage(param1, param2, callback) ... 我正在尝试您的async/await 示例,但我遇到了问题。我没有实例化new Promise,而是调用.Get(),因此无法访问任何resolve() 方法。因此,我的 getMessage() 返回的是 Promise 而不是结果。你能稍微编辑一下你的答案以显示一个有效的语法吗? @InteXX 我不确定你打电话给.Get() 是什么意思。可能最好发布一个新问题。【参考方案4】:

显而易见,杯子代表outerScopeVar

异步函数就像...

【讨论】:

而尝试使异步函数同步执行将尝试在 1 秒内喝完咖啡,并在 1 分钟内将咖啡倒进你的腿上。 如果它说的很明显,我不认为这个问题会被问到,不是吗? @broccoli2000 我并不是说这个问题很明显,但很明显杯子在图中代表什么:) 如果杯子是变量,那么函数在哪里?【参考方案5】:

其他答案非常好,我只想提供一个直截了当的答案。仅限于 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 请求完成执行。

假设发布请求需要 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 通过使用回调方法 有快速的语法示例吗?【参考方案6】:

在所有这些场景中,outerScopeVar 被修改或分配一个值异步发生在稍后的时间(等待或侦听某个事件的发生),当前执行将不要等待。所以所有这些情况当前的执行流程都会导致outerScopeVar = undefined

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

1.

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

2.

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

3.

这次ajax回调。

4.

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

5.

明显的承诺(将来会做一些事情)是异步的。见What are the differences between Deferred, Promise and Future in JavaScript?

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

【讨论】:

以上是关于为啥我的变量在函数内部修改后没有改变? - 异步代码参考的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

添加回调函数后,为啥我的变量在里面修改不变?

为啥在单独的线程中修改时我的变量没有改变? c++/升压