关于javascript:为什么我的变量在函数内部修改后没有改变?

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

在下面的例子中,为什么outerScopeVar在所有情况下都是未定义的?

1
2
3
4
5
6
7
8
var outerScopeVar;

var img = document.createElement('img');
img.onload = function() {
    outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
1
2
3
4
5
var outerScopeVar;
setTimeout(function() {
    outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
1
2
3
4
5
6
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
});
alert(outerScopeVar);
1
2
3
4
5
6
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
    outerScopeVar = data;
});
console.log(outerScopeVar);
1
2
3
4
5
6
// with promises
var outerScopeVar;
myPromise.then(function (response) {
    outerScopeVar = response;
});
console.log(outerScopeVar);
1
2
3
4
5
6
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

在所有这些例子中,它为什么输出undefined?我不想有什么解决办法,我想知道为什么会这样。

Note: This is a canonical question for JavaScript asynchronicity. Feel free to improve this question and add more simplified examples which the community can identify with.


一句话回答:异步性。好的。前言

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

  • @FelixKling的"如何从Ajax调用返回响应"。请参阅他解释同步和异步流以及"重构代码"部分的出色答案。@BenjaminGruenbaum也在同一个线程中努力解释异步性。好的。

  • @MatteEsch对"从fs.readfile获取数据"的回答也非常简单地解释了异步性。好的。

手头问题的答案

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

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

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

  • 在将来某个时候,当(和如果)成功加载图像时,可以调用img.onload
  • setTimeout在延迟结束后,clearTimeout没有取消超时后,可以在将来某个时候调用。注意:即使使用0作为延迟,所有浏览器都有一个最小的超时延迟上限(在HTML5规范中指定为4ms)。
  • jquery $.post的回调可能在将来某个时候调用,此时(如果)Ajax请求已成功完成。
  • 当文件被成功读取或抛出错误时,可以在将来某个时候调用node.js的fs.readFile

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

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

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

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

async code highlighted好的。

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

这真的很简单。依赖异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,在回调函数中移动alertconsole.log也会输出预期的结果,因为此时结果是可用的。好的。实现自己的回调逻辑

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

1
2
3
4
5
6
7
8
9
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

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

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

这个示例显然与其他示例面临相同的问题,它不会等待异步函数执行。好的。

让我们来实现我们自己的回调系统。首先,我们去掉那个丑陋的outerScopeVar,在这种情况下,它是完全无用的。然后我们添加一个接受函数参数的参数,我们的回调。当异步操作完成时,我们调用这个回调来传递结果。实施(请按顺序阅读意见):好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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);
}

好的。

通常在实际用例中,domAPI和大多数库已经提供了回调功能(本演示示例中的helloCatAsync实现)。您只需要传递回调函数,并了解它将在同步流之外执行,然后重新构造代码以适应这种情况。好的。

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

您将不得不使用回调模式,或者……承诺。好的。承诺

尽管有一些方法可以让回调地狱与普通JS抗衡,但承诺正在流行,目前在ES6中正被标准化(参见Promise-MDN)。好的。

Promises(又称A.K.A.Futures)提供了一种更为线性的、令人愉快的异步代码阅读方式,但是解释它们的全部功能超出了这个问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:好的。

  • 摇滚乐队1年前
  • 你错过了诺言的一角

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

  • 节点背部的艺术解释了异步码,并以Vanilla JS为例和Node.JS代码非常好。

Note: I've marked this answer as Community Wiki, hence anyone with at least 100 reputations can edit and improve it! Please feel free to improve this answer, or submit a completely new answer if you'd like as well.

Ok.

I want to turn this question into a canonical topic to answer asynchronicity issues which are unrelated to Ajax (there is How to return the response from an AJAX call? for that), hence this topic needs your help to be as good and helpful as possible!

Ok.

好吧


Fabr_cio的答案很到位;但我想用一些技术性较低的东西来补充他的答案,这些东西集中在一个类比上,以帮助解释异步性的概念。

类比…

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

Me: Hi Bob, I need to know how we foo'd the bar'd last week. Jim wants a report on it, and you're the only one who knows the details about it.

Bob: Sure thing, but it'll take me around 30 minutes?

Me: That's great Bob. Give me a ring back when you've got the information!

这时,我挂断了电话。因为我需要鲍勃的信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我收到了一些电子邮件。40分钟后(鲍勃很慢),鲍勃回电给我需要的信息。在这一点上,我恢复了我的报告工作,因为我已经掌握了我需要的所有信息。

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

Me: Hi Bob, I need to know how we foo'd the bar'd last week. Jim want's a report on it, and you're the only one who knows the details about it.

Bob: Sure thing, but it'll take me around 30 minutes?

Me: That's great Bob. I'll wait.

我坐在那里等着。等待着。等待着。40分钟。除了等待什么都不做。最后,鲍勃给了我信息,我们挂断了电话,我完成了我的报告。但我失去了40分钟的工作效率。

这是异步与同步行为

这正是我们所讨论的所有例子中发生的事情。加载图像、从磁盘加载文件以及通过Ajax请求页面都是缓慢的操作(在现代计算环境中)。

Javascript不需要等待这些缓慢的操作完成,而是让您注册一个回调函数,当缓慢的操作完成时将执行该函数。但与此同时,javascript将继续执行其他代码。事实上,JavaScript在等待缓慢的操作完成的同时执行其他代码,这使得行为同步。如果javascript在执行任何其他代码之前等待操作完成,那么这将是同步行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
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变量声明为全局变量。

1
2
3
4
5
6
7
8
var img = document.createElement('img');

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

img.src = 'lolcat.png';

您将始终看到回调被指定为函数,因为这是JavaScript中定义某些代码的唯一*方法,但直到稍后才执行它。

因此,在我们的所有示例中,function() { /* Do something */ }是回调;要修复所有示例,我们所要做的就是将需要操作响应的代码移入其中!

*从技术上讲,你也可以使用eval(),但eval()是邪恶的。

我如何让我的来电者等待?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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()的函数注册回调,然后将宽度警报移动到该回调内;

1
2
3
4
5
6
7
8
9
10
11
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)。


下面是一个更为简洁的答案,适用于寻求快速参考的人,以及一些使用承诺和异步/等待的示例。

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

1
2
3
4
5
6
7
8
function getMessage() {
  var outerScopeVar;
  setTimeout(function() {
    outerScopeVar = 'Hello asynchronous world!';
  }, 0);
  return outerScopeVar;
}
console.log(getMessage());

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

解决这一问题的两个主要方法是使用回调和承诺:

回调

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

1
2
3
4
5
6
7
8
function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});

承诺

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

1
2
3
4
5
6
7
8
9
10
11
function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

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

jquery延迟

jquery提供的功能与承诺的延迟类似。

1
2
3
4
5
6
7
8
9
10
11
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函数内同步使用promises:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

异步函数就像…

asynchronous call for coffee


其他答案都很好,我只想直接回答这个问题。仅限于jquery异步调用

所有Ajax调用(包括$.get$.post$.ajax都是异步的。

考虑你的例子

1
2
3
4
5
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秒之后设置。

试一试,

1
2
3
4
5
6
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请求返回了一些值。然后,当您单击"确定"时,在"警报"框中,下一个警报将打印预期值,因为您在等待它。

在现实场景中,代码变成,

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

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


在所有这些场景中,outerScopeVar被异步地修改或赋值,或者在以后发生(等待或监听某个事件发生),当前的执行不会等待,所以所有这些场景中当前的执行流都会导致outerScopeVar = undefined

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

1。

enter image description here

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

2。

2

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

三。

enter image description here这次是Ajax回调。

4。

enter image description here

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

5。

enter image description here

显而易见的承诺(将来会做些什么)是异步的。看看在javascript中延迟、承诺和未来之间有什么区别?

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