提问人:Fabrício Matté 提问时间:5/15/2014 最后编辑:Christian Vincenzo TrainaFabrício Matté 更新时间:12/25/2022 访问量:299304
为什么我的变量在函数内部修改后没有改变?- 异步代码参考
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
问:
鉴于以下示例,为什么在所有情况下都是未定义的?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 异步性的规范问题。随意改进这个问题,并添加社区可以识别的更简化的例子。
答:
一个字的答案:异步性。
前言
这个主题在 Stack Overflow 中至少已经迭代了几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling 对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流的出色答案,以及“重构代码”部分。
@Benjamin Gruenbaum 也花了很多精力在同一线程中解释异步性。@Matt Esch 对“从 fs.readFile 获取数据”的回答也以一种简单的方式很好地解释了异步性。
手头问题的答案
让我们先跟踪常见行为。在所有示例中,函数内部都修改了 。该功能显然不会立即执行;它作为参数被赋值或传递。这就是我们所说的回调。outerScopeVar
现在的问题是,什么时候调用该回调?
这取决于具体情况。让我们再次尝试跟踪一些常见行为:
img.onload
可能会在将来的某个时间(以及如果)映像已成功加载时调用。setTimeout
可能会在延迟到期且超时未被取消后在将来的某个时间调用。注意:即使使用 as 延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4ms)。clearTimeout
0
- jQuery 的回调可能会在将来的某个时候(以及如果)Ajax 请求已成功完成时调用。
$.post
- Node.js 可能会在将来的某个时候被调用,当文件被成功读取或抛出错误时。
fs.readFile
在所有情况下,我们都有一个回调,可能会在将来的某个时候运行。这种“将来的某个时候”就是我们所说的异步流。
异步执行被推出同步流。也就是说,在同步代码堆栈执行时,异步代码永远不会执行。这就是 JavaScript 是单线程的含义。
更具体地说,当 JS 引擎处于空闲状态时——不执行(a)同步代码堆栈——它将轮询可能触发异步回调的事件(例如过期超时、收到的网络响应)并一个接一个地执行它们。这被视为事件循环。
也就是说,手绘红色形状中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行后执行:
简而言之,回调函数是同步创建的,但执行是异步的。在知道异步函数已被执行之前,你不能依赖它的执行,以及如何做到这一点?
这很简单,真的。依赖于异步函数执行的逻辑应从此异步函数内部启动/调用。例如,在回调函数中移动 s 和 s 将输出预期结果,因为该结果在该点可用。alert
console.log
实现自己的回调逻辑
通常,您需要对异步函数的结果执行更多操作,或者根据异步函数的调用位置对结果执行不同的操作。让我们处理一个更复杂的例子:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
注意:我使用随机延迟作为通用异步函数;相同的示例适用于 Ajax、、 和任何其他异步流。setTimeout
readFile
onload
这个例子显然遇到了与其他例子相同的问题;它不会等到异步函数执行。
让我们通过实现我们自己的回调系统来解决它。首先,我们摆脱了在这种情况下完全无用的丑陋。然后我们添加一个接受函数参数的参数,即我们的回调。当异步操作完成后,我们调用此回调,传递结果。实现(请按顺序阅读评论):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 异步性的更多阅读材料
- The Art of Node - Callbacks 通过普通的 JS 示例和 Node.js 代码很好地解释了异步代码和回调。
注意:我已将此答案标记为社区维基。因此,任何拥有至少 100 个声誉的人都可以编辑和改进它!如果您愿意,请随时改进此答案或提交全新的答案。
我想把这个问题变成一个规范的主题,来回答与Ajax无关的异步性问题(有 如何返回AJAX调用的响应?),因此这个主题需要你的帮助,尽可能地好和有帮助!
评论
法布里西奥的回答很到位,但我想用一些不那么技术性的东西来补充他的回答,它侧重于类比来帮助解释异步性的概念。
打个比方......
昨天,我正在做的工作需要一位同事提供一些信息。我给他打了电话;谈话是这样的:
我:嗨,鲍勃,我需要知道我们上周是怎么玩酒吧的。吉姆想要一份关于它的报告,而你是唯一知道细节的人。
鲍勃:当然可以,但是我需要大约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.png
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()
对于这个目的来说是邪恶的
如何让我的来电者等待?
您当前可能有一些与此类似的代码;
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()
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
评论
img.onload=function(){..
function img.onload(){..
对于正在寻找快速参考的人来说,这里有一个更简洁的答案,以及一些使用 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 和 Q 等库中实现。
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 环境包括对 async
和 await
的支持(如 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();
评论
function getMessage(param1, param2, callback) {...}
在所有这些场景中,异步修改或分配一个值,或者在以后发生(等待或侦听某个事件发生),当前执行不会等待。因此,所有这些情况下,当前的执行流程都会导致outerScopeVar
outerScopeVar = undefined
让我们讨论每个示例(我标记了异步调用或延迟调用以发生某些事件的部分):
1.
在这里,我们注册一个 eventlistner,它将在该特定事件上执行。这里加载图像。然后当前执行与下一行连续,同时事件可能不会发生。即,功能等待引用的图像异步加载。这将在以下所有示例中发生 - 事件可能会有所不同。img.src = 'lolcat.png';
alert(outerScopeVar);
img.onload
2.
在这里,timeout 事件扮演的角色,它将在指定时间后调用处理程序。这里是 ,但它仍然注册了一个异步事件,它将被添加到执行的最后一个位置,从而保证延迟。0
Event Queue
3.
4.
Node 可以看作是异步编码之王。这里标记的函数被注册为回调处理程序,将在读取指定文件后执行。
5.
显而易见的承诺(将来会做一些事情)是异步的。请参阅 JavaScript 中的 Deferred、Promise 和 Future 之间有什么区别?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript
显而易见的是,杯子代表 .outerScopeVar
异步函数就像...
评论
其他答案都很好,我只想提供一个直截了当的答案。仅限制为 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);
});
依赖于异步调用的所有代码都在异步块内移动,或者通过等待异步调用来移动。
简短的回答是:异步性。
为什么需要异步?
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 没有给我们一种方法来做到这一点。此外,这是加载一个图像。如果我们想知道一组图像何时加载,事情会变得更加复杂。
活动并不总是最好的方式
事件非常适合在同一对象上可能多次发生的事情,等等。对于这些事件,您并不真正关心在附加侦听器之前发生了什么。keyup
touchstart
正确执行此操作的两种主要方法:回调和承诺。
回调
回调是在其他函数的参数中传递的函数,此过程在 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);
});
评论