Javascript 为什么我的变量在函数内部修改后保持不变异步代码引用

Javascript 为什么我的变量在函数内部修改后保持不变异步代码引用,javascript,asynchronous,Javascript,Asynchronous,给出以下示例,为什么在所有情况下都未定义outerScopeVar var outerScopeVar; var img = document.createElement('img'); img.onload = function() { outerScopeVar = this.width; }; img.src = 'lolcat.png'; alert(outerScopeVar); 为什么在所有这些示例中它都输出未定义?我不想采取变通办法,我想知道为什么会发生这种情况 注意:

给出以下示例,为什么在所有情况下都未定义outerScopeVar

var outerScopeVar;

var img = document.createElement('img');
img.onload = function() {
    outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
为什么在所有这些示例中它都输出未定义?我不想采取变通办法,我想知道为什么会发生这种情况

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

一个词的答案是:异步性

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

。请参阅他解释同步和异步流的优秀答案,以及重构代码部分。 @Benjamin Gruenbaum也花了大量精力解释同一线程中的异步性

还以简单的方式很好地解释了异步性

对眼前问题的回答 让我们首先追踪常见的行为。在所有示例中,outerScopeVar都是在函数内部修改的。该函数显然不是立即执行的,而是作为参数赋值或传递的。这就是我们所说的回调

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

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

img.onload可能会在将来的某个时候调用,如果映像已成功加载。 setTimeout可能在将来的某个时候被调用,在延迟过期并且clearTimeout没有取消超时之后。注意:即使使用0作为延迟,在HTML5规范中,所有浏览器都将最小超时延迟上限指定为4ms。 jQuery$.post的回调可能在将来的某个时候调用,如果Ajax请求成功完成。 将来,当文件被成功读取或抛出错误时,可能会调用Node.js的fs.readFile。 在所有情况下,我们都有一个回调,它可能在将来某个时候运行。这在将来的某个时候就是我们所说的异步流

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

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

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

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

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

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

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

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}
注意:我使用带有随机延迟的setTimeout作为通用异步函数,相同的示例适用于Ajax、readFile、onload和任何其他异步流

此示例显然与其他示例存在相同的问题,它不会等待异步函数执行

让我们通过实现我们自己的回调系统来解决这个问题。首先,我们去掉了那个丑陋的外套,在这种情况下它是完全无用的。然后我们添加一个接受函数参数的参数,我们的回调函数。异步操作完成后,我们调用此回调传递结果。实施请按顺序阅读评论:

// 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. 通过回调函数调用helloCatAsync, //接收异步操作的结果时将调用 console.log1。函数名为。。。 helloCatAsyncfunctionresult{ //5.从异步函数接收结果, //现在用它做你想做的事: console.log5.结果为:,结果; }; // 2. 回调参数是对函数的引用,该函数 //作为helloCatAsync调用的参数传递 函数HellocateAsyncCallback{ callback这里是作为上述参数传递的函数。。。 //3.启动异步操作: setTimeoutfunction{ 欺骗 ole.log3。启动异步操作。。。 控制台。log4。已完成异步操作、调用回调、传递结果。。。 // 4. 已完成异步操作, //调用将结果作为参数传递的回调函数 回调'Nya'; },Math.random*2000; } 法布里西奥的回答很贴切;但我想用一些不那么技术性的东西来补充他的回答,重点放在一个类比上,以帮助解释异步性的概念

一个类比。。。 昨天,我正在做的工作需要一位同事提供一些信息。我给他打电话;下面是对话的过程:

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

鲍勃:当然可以,但要花我30分钟左右

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

这时,我挂断了电话。因为我需要鲍勃提供信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我又收到了一些电子邮件。40分钟后,鲍勃反应迟钝,鲍勃回电话给我需要的信息。在这一点上,我继续我的工作与我的报告,因为我有我需要的所有信息

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

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

鲍勃:当然可以,但要花我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,这是一个slooow操作。回调函数将在这个缓慢的操作完成后执行,但与此同时,JavaScript将继续处理下一行代码;i、 e.alertouterScopeVar

这就是为什么我们看到警报显示未定义;因为警报是立即处理的,而不是在加载图像之后

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

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

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

img.src = 'lolcat.png';
您总是会看到回调被指定为函数,因为这是JavaScript中定义某些代码的唯一*方法,但在以后才执行它

因此,在我们所有的例子中,{/*dosomething*/}函数是回调函数;要修复所有示例,我们所要做的就是将需要操作响应的代码移到其中

*从技术上讲,您也可以使用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);
});

。。。与前面一样,请注意,我们已经能够删除本例中的全局变量。这里有一个更简洁的答案,供那些正在寻找快速参考的人以及一些使用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接受回调参数,一旦可用,将调用该参数以将结果传递回调用代码

function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});
承诺提供了一种比回调更灵活的替代方案,因为它们可以是自然的 可以灵活组合以协调多个异步操作。node.js 0.12+和许多当前浏览器本机提供了一个标准实现,但也在和等库中实现

jQuery

jQuery提供了类似于承诺的功能

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

getMessage().done(function(message) {
  console.log(message);  
});
异步/等待

如果您的JavaScript环境包括对Node.js 7.6+等的支持,那么您可以在异步函数中同步使用承诺:

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();
在所有这些场景中,outerScopeVar被异步修改或分配一个值,或者在稍后的时间发生,等待或侦听某个事件的发生,而当前执行不会等待该事件的发生。因此,所有这些情况下,当前执行流都会导致outerScopeVar=undefined

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

一,

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

二,

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

三,

这次是ajax回调

四,

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

显然,将来要做的事情是异步的。看


最明显的是,这个杯子代表了外层

异步函数应该像


其他答案都很好,我只想提供一个直截了当的答案。仅限于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请求返回了一些值。然后,当您单击“确定”时,在“警报”框中,下一个警报将打印预期值,因为您正在等待它

在现实生活中,代码变成

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

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

相关元讨论-@Dukeling谢谢,我很确定我已经用该链接发表了评论,但显然有一些缺少的评论。另外,关于您的编辑:我相信在标题中具有规范性和异步性有助于搜索此问题以将另一个问题标记为重复问题。当然,在寻找异步性解释时,它也有助于从Google中找到这个问题。仔细想想,规范异步性主题的标题有点重,异步代码参考更简单、更客观。我还相信大多数人搜索异步而不是异步。有些人在函数调用之前初始化他们的变量。换一个能代表这一点的标题怎么样?比如,为什么我的变量在我在函数内部修改后保持不变?在上面提到的所有代码示例中,alertouterScopeVar;现在执行,而将值赋值给outerScopeVar则是异步的。而试图使异步函数同步运行则是试图在1秒内喝咖啡,并在1分钟内将咖啡倒在你的膝盖上。如果这是显而易见的,我不认为会有人问这个问题,不是吗?@broccoli2000我的意思不是说问题很明显,但很明显,杯子在图纸中代表了什么:你的承诺样本基本上就是我在过去几个小时里一直在寻找的。你的例子很好,同时也解释了承诺。为什么这在其他地方不存在是令人难以置信的。这一切都很好,但是如果您需要使用参数调用getMessage呢?在这种情况下,您将如何编写上述内容?@Chiwda您只需将回调参数放在最后:函数getMessageparam1,param2,回调{…}。我正在尝试您的async/await示例,但遇到了问题。而不是实例化一个新的Pr
omise,我正在进行.Get调用,因此无法访问任何解析方法。因此,我的getMessage返回的是承诺,而不是结果。你能稍微修改一下你的答案来显示这个问题的有效语法吗?@InteXX我不知道你打.Get电话是什么意思。可能最好发布一个新问题。在上一个示例中,您使用匿名函数的具体原因是什么,还是使用命名函数的工作原理是相同的?代码示例有点奇怪,因为您在调用函数后声明了该函数。当然是因为吊装,但这是故意的吗?是死锁吗。felix kling指的是您的答案,您指的是felix answer您需要了解红圈代码只是异步的,因为它是由本机异步javascript函数执行的。无论是Node.js还是浏览器,这都是javascript引擎的一项功能。它是异步的,因为它是作为回调传递给一个函数的,该函数本质上是一个用C等实现的黑盒。。。对于倒霉的开发人员来说,它们是异步的……只是因为。如果您想编写自己的异步函数,必须将其发送到SetTimeoutmyfunc,0来破解它。你应该那样做吗?另一个争论…可能不是。@Fabricio我已经找到了定义>=4ms夹具的规范,但是找不到它-我在MDN上发现了一些类似的钳制嵌套调用的机制-是否有人有到HTML规范正确部分的链接。但是,如果您想在不同的计算中使用结果,或者将结果存储在对象变量中,则向控制台发送警报或消息如何有用呢?这是最有用的,快速简洁地回答整个问题。谢谢。或者通过等待异步调用,如何做到这一点?@InteXX通过使用回调方法,您有一个快速语法示例吗?
function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

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

getMessage().done(function(message) {
  console.log(message);  
});
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();
var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3
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
var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
    alert(outerScopeVar);
});