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

给出以下示例,为什么在所有情况下都未定义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异步性的典型问题。随时改进此问题,并添加更多简化的示例,社区可以识别。

标签:javascript,asynchronous

回答1:

一个单词的答案:异步

前言

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


眼前问题的答案

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

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

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

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

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

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

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

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

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

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

实现自己的回调逻辑

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

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实现)。您只需要传递回调函数,并了解它将在同步流中执行,并重新组织代码以适应该情况。

您还将注意到,由于异步性质,不可能将代码从异步流返回到定义回调的同步流,因为异步回调执行时间很长在同步代码已经完成执行之后。

代替从异步回调中返回代码,您将不得不利用回调模式,或者……应允。

承诺

尽管有很多方法可以将回调地狱与香草JS隔离开来,但承诺日益普及,并且目前正在普及已在ES6中标准化(请参见承诺-MDN )。

Promise(又称期货)提供了一种更加线性,因此令人愉悦的异步代码读取方法,但是解释其全部功能不在此问题范围之内。相反,我会将这些出色的资源留给有兴趣的人


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


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

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

回答2:

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


类比...

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

:鲍勃,您好,我想知道上周我们如何 foo bar 进行了操作。吉姆想要一份报告,而您是唯一知道有关该报告的细节的人。

鲍勃:好的,但是大约需要30分钟吗?

:鲍勃,太好了。知道了,给我回电话!

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


想象一下对话是否像这样进行;

:鲍勃,您好,我想知道上周我们如何 foo bar 进行了操作。吉姆想要一份报告,而您是唯一知道有关该报告的细节的人。

鲍勃:好的,但是大约需要30分钟吗?

:鲍勃,太好了。我等。

我坐在那里等。并等待。并等待。 40分钟。除了等待,什么都不做。最终,鲍勃给了我信息,我们挂断了电话,我完成了报告。但是我失去了40分钟的生产力。


这是异步行为还是同步行为

这正是我们问题中所有示例所发生的。加载映像,从磁盘加载文件以及通过AJAX请求页面都是缓慢的操作(在现代计算的背景下)。

您无需注册等待完成这些缓慢的操作,而是让您注册一个回调函数,该回调函数将在缓慢的操作完成后执行。但是,与此同时,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(){/*做一些*/}是回调;要修复 all 示例,我们要做的就是将需要操作响应的代码移到其中!

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

但是,我们现在知道returnexternalScopeVar会立即发生。在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)。

回答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

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

回调

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

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

承诺

Promise提供了一种比回调更灵活的选择,因为它们可以自然地组合以协调多个异步操作。 Promises / A + 标准实现是在node.js(0.12+)和许多当前浏览器中本地提供的,但也已实现在诸如 Bluebird 问题

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

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

jQuery 延迟

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环境包括对 异步 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();

回答4:

为了说明明显,杯子代表outerScopeVar

异步功能就像...

回答5:

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

所有ajax调用(包括$.get$.post$.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);
});

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

回答6:

在所有这些情况下,outerScopeVar都被修改或异步在以后的某个时间(等待或监听某些事件的发生)中分配了一个值,用于当前执行不会等待。因此,所有这些情况下,当前执行流程都会导致outerScopeVar=undefined

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

1。

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

2。

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

3。

这次是ajax回调。

4。

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

5。

明显的承诺(将来会做)是异步的。参见 Deferred,Promise和JavaScript的未来?

https:/ /www.quora.com/在Java脚本中的承诺和回调之间有什么区别

回到顶部