javascript—为什么在函数内部修改变量后,变量保持不变异步代码引用

neskvpey  于 2021-09-23  发布在  Java
关注(0)|答案(2)|浏览(467)

给出以下示例,为什么是 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异步性的一个典型问题。请随意改进此问题,并添加更多社区可以认同的简化示例。

r6hnlfcb

r6hnlfcb1#

一个词的答案是:异步性。

前言

在堆栈溢出中,这个主题已经被迭代了至少几千次。因此,首先我想指出一些非常有用的资源:
@felix kling对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流的优秀答案,以及“重构代码”部分。
@benjamin gruenbaum也花了大量精力解释同一线程中的异步性。
@matt esch对“从fs.readfile获取数据”的回答也以一种简单的方式很好地解释了异步性。

对眼前问题的回答

让我们首先追踪常见的行为。在所有示例中 outerScopeVar 在函数内部修改。该函数显然不是立即执行的,而是作为参数赋值或传递的。这就是我们所说的回调。
现在的问题是,什么时候调用回调?
这要视情况而定。让我们再次尝试跟踪一些常见行为: img.onload 可能在将来的某个时候,当(并且如果)映像已成功加载时调用。 setTimeout 可能在延迟过期且超时未被取消后的将来某个时间调用 clearTimeout . 注意:即使在使用 0 作为延迟,所有浏览器都有一个最小超时延迟上限(在html5规范中指定为4ms)。
jquery $.post 的回调可能在将来某个时候调用,此时(如果)ajax请求已成功完成。
js的 fs.readFile 可能在将来的某个时候,当文件被成功读取或抛出错误时调用。
在所有情况下,我们都有一个回调,它可能在将来某个时候运行。这个“将来某个时候”就是我们所说的异步流。
异步执行从同步流中推出。也就是说,当同步代码堆栈正在执行时,异步代码永远不会执行。这就是javascript是单线程的含义。
更具体地说,当js引擎空闲时(不执行一堆同步代码),它将轮询可能触发异步回调的事件(例如过期超时、收到的网络响应),并逐个执行。这被视为事件循环。
也就是说,手绘红色形状中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行后执行:

简而言之,回调函数是同步创建的,但异步执行。在知道异步函数已经执行之前,您不能依赖于它的执行,如何做到这一点?
其实很简单。依赖于异步函数执行的逻辑应该从此异步函数内部启动/调用。例如,移动 alert s和 console.log 回调函数中的s也将输出预期结果,因为此时结果可用。

实现自己的回调逻辑

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

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

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

注意:我正在使用 setTimeout 将随机延迟作为通用异步函数,同样的示例适用于ajax, 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 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);
}

在实际用例中,domapi和大多数库都已经提供了回调功能(回调函数) helloCatAsync 本演示示例中的实现)。您只需要传递回调函数并了解它将在同步流之外执行,然后重新构造代码以适应这一点。
您还将注意到,由于异步特性,不可能 return 从异步流返回到定义回调的同步流的值,因为异步回调是在同步代码已经完成执行很久之后执行的。
而不是 return 要从异步回调中提取值,您必须使用回调模式,或者。。。承诺。

许诺

尽管有一些方法可以让vanilla js远离回调地狱,但承诺正变得越来越受欢迎,目前正在es6中标准化(请参阅promise-mdn)。
promises(又称futures)提供了一种更线性的异步代码阅读方式,因此也更令人愉快,但解释它们的全部功能超出了本问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:
javascript承诺-html5的岩石
你错过了承诺的重点-domenic.me

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

node-callbacks的艺术用普通的js示例和node.js代码很好地解释了异步代码和回调。
注意:我已经将这个答案标记为社区维基,因此任何拥有至少100个声誉的人都可以编辑和改进它!请随时改进此答案,如果您愿意,也可以提交一个全新的答案。
我想把这个问题转化为一个规范的主题,来回答与ajax无关的异步性问题(这里有一个如何从ajax调用返回响应的问题?因此,本主题需要您的帮助,使其尽可能好且有帮助!

gzszwxb4

gzszwxb42#

法布里西奥的回答很贴切;但我想用一些不那么技术性的东西来补充他的回答,重点放在一个类比上,以帮助解释异步性的概念。

一个类比。。。

昨天,我正在做的工作需要一位同事提供一些信息。我给他打电话;下面是对话的过程:
我:嗨,鲍勃,我想知道我们上周在酒吧是怎么过的。吉姆想要一份报告,而你是唯一知道细节的人。
鲍勃:当然可以,但要花我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 ,这是一个单桅帆船操作。回调函数将在这个缓慢的操作完成后执行,但与此同时,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() 这是邪恶的

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

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

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() 返回

相关问题