提问者:小点点

为什么在函数内部修改变量后,变量没有被修改? -异步代码引用


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

var outerScopeVar;

var img = document.createElement('img');
img.onload = function() {
    outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);

null

var outerScopeVar;
setTimeout(function() {
    outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);

null

// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
});
alert(outerScopeVar);

null

// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
    outerScopeVar = data;
});
console.log(outerScopeVar);

null

// with promises
var outerScopeVar;
myPromise.then(function (response) {
    outerScopeVar = response;
});
console.log(outerScopeVar);

null

// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

为什么在所有这些示例中它都输出undefined? 我不想要变通办法,我想知道为什么会发生这种事。

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


共3个答案

匿名用户

一个字回答:异步性。

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

>

  • @Felix Kling对“如何从异步调用返回响应?”的回答。 请参阅他解释同步和异步流的出色回答,以及“重构代码”部分。
    @Benjamin Gruenbaum也花了大量精力解释同一线程中的异步性。

    @Matt Esch对“从fs.readfile获取数据”的回答也以一种简单的方式非常好地解释了异步性。

    让我们先追踪常见的行为。 在所有示例中,OuterScopeVar都是在函数内部修改的。 该函数显然不是立即执行的,它是作为参数被赋值或传递的。 这就是我们所说的回调。

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

    这要看案情了。 让我们再次尝试追踪一些常见的行为:

      可能在将来某个时候调用
    • img.onload,此时(以及如果)成功加载了映像。
    • 在延迟过期且超时未被ClearTimeout取消之后,
    • SetTimeout可能会在将来某个时候调用。 注意:即使使用0作为延迟,所有浏览器都有一个最小超时延迟上限(在HTML5规范中指定为4ms)。
    • jQuery$.post的回调可能会在将来某个时候调用,此时(以及如果)Ajax请求已经成功完成。
    • node.js的fs.readfile可能会在将来某个时候调用,当文件已成功读取或抛出错误时。

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

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

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

    也就是说,在手绘红色形状中高亮显示的异步代码可以仅在它们各自代码块中的所有剩余同步代码都已执行之后才执行:

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

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

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

    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);
    }
    

    上面示例的代码段:

    null

    // 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);
    }

  • 匿名用户

    Fabrício的回答恰如其分; 但是我想用一些不那么技术性的东西来补充他的回答,它集中于一个类比来帮助解释异步性的概念。

    昨天,我正在做的工作需要一位同事提供一些信息。 我给他打了电话; 对话内容如下:

    我:嗨,鲍勃,我想知道我们上星期在酒吧吃得怎么样。 吉姆想要一份关于这件事的报告,而你是唯一一个知道这件事的细节的人。

    鲍勃:当然可以,不过大约要花30分钟?

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

    说着,我挂断了电话。 因为我需要从鲍勃那里得到信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我赶上了一些电子邮件。 40分钟后(鲍勃行动迟缓),鲍勃回电,给了我需要的信息。 在这一点上,我恢复了我的报告工作,因为我已经掌握了我所需要的所有信息。

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

    我:嗨,鲍勃,我想知道我们上星期在酒吧吃得怎么样。 Jim Want是关于这件事的报道,而你是唯一一个知道这件事的细节的人。

    鲍勃:当然可以,不过大约要花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,这是一个sloooow操作。 回调函数将在这个缓慢的操作完成后执行,但与此同时,JavaScript将继续处理接下来的代码行; 即Alert(outerScopeVar)

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

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

    var img = document.createElement('img');
    
    img.onload = function() {
        var localScopeVar = this.width;
        alert(localScopeVar);
    };
    
    img.src = 'lolcat.png';
    

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

    因此,在我们的所有示例中,函数(){/*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);
    

    但是,我们现在知道返回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)。

    匿名用户

    这里有一个更简洁的答案,供那些正在寻找快速参考的人使用,还有一些使用promises和async/await的示例。

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

    function getMessage() {
      var outerScopeVar;
      setTimeout(function() {
        outerScopeVar = 'Hello asynchronous world!';
      }, 0);
      return outerScopeVar;
    }
    console.log(getMessage());
    

    在这种情况下,未定义会被记录,因为GetMessage在调用SetTimeout回调之前返回,并更新OuterScopeVar

    解决它的两种主要方法是使用回调和承诺:

    回调

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

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

    承诺

    承诺提供了一种比回调更灵活的替代方案,因为它们可以自然地组合在一起以协调多个异步操作。 在Node.js(0.12+)和许多当前浏览器中提供了一个Promises/A+标准实现,但也在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提供的功能类似于带有延期的承诺。

    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函数中同步使用承诺:

    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();
    

    相关问题