8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

为什么我在函数内部修改变量后它保持不变? - 异步代码参考

MP-SATARA 1月前

145 0

给出以下示例,为什么在所有情况下 outerScopeVar 都未定义?var outerScopeVar;var img = document.createElement('img');img.onload = function() { outerScopeVar = this.width;};...

给出以下例子,为什么 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 异步性 的典型问题 。欢迎改进此问题并添加更多社区可以识别的简化示例。

帖子版权声明 1、本帖标题:为什么我在函数内部修改变量后它保持不变? - 异步代码参考
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由MP-SATARA在本站《object》版块原创发布, 转载请注明出处!
最新回复 (0)
  • @Dukeling 谢谢,我很确定我已经对该链接进行了评论,但显然有一些评论缺失了。另外,关于您的编辑:我相信在标题中使用“canonical”和“asynchronicity”有助于在搜索此问题时将另一个问题标记为重复问题。当然,在寻找异步性解释时,它也有助于从 Google 找到此问题。

  • 再想想,“规范异步性主题”这个标题有点沉重,“异步代码参考”更简单、更客观。我也相信大多数人搜索的是“异步”,而不是“异步性”。

  • ZRep 1月前 0 只看Ta
    引用 4

    有些人在函数调用之前初始化变量。那么如何改变标题来表示这一点呢?比如“为什么我的变量

  • 在您上面提到的所有代码示例中,\'alert(outerScopeVar);\' 立即执行,而将值分配给\'outerScopeVar\' 则在稍后(异步)执行。

  • 答案只有一个词: 异步性 .

    前言

    这个话题在 Stack Overflow 上至少被重复了几千次。因此,首先我想指出一些非常有用的资源:

    • p2

    • p3


    当前问题的答案

    让我们首先追溯一下常见的行为。在所有示例中,都 outerScopeVar 函数 内部进行了修改 。该函数显然不会立即执行;它被赋值或作为参数传递。这就是我们所说的 回调 .

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

    这要视情况而定。让我们再次尝试追踪一些常见行为:

    • img.onload 当图像成功加载时(如果成功加载), 在将来的某个时间 被调用
    • setTimeout 可能会 在延迟到期后某个时间 ,并且超时尚未被取消 clearTimeout 。注意:即使使用 0 延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4 毫秒)。
    • 当 Ajax 请求成功完成时(并且如果成功完成), $.post jQuery 的回调可能会 在将来的某个时间
    • 当文件读取成功或者抛出错误时, fs.readFile Node.js 可能会 在将来的某个时间

    在未来某个时间 运行的回调 。这个“未来某个时间”就是我们所说的 异步流 .

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

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

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

    async code highlighted

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

    其实很简单。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,在回调函数内部移动 s alert 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 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 从异步回调中获取值,而必须使用回调模式,或者......承诺。

    承诺

    尽管有一些方法可以通过 vanilla JS 避免 回调地狱 ,但 Promise 越来越受欢迎,并且目前正在 ES6 中进行标准化(请参阅 Promise - MDN )。

    Promises(又名 Futures)提供了更线性、更令人愉悦的异步代码阅读体验,但解释其全部功能超出了本文的范围。相反,我将为感兴趣的人留下这些优秀的资源:

    • JavaScript 承诺 - HTML5 Rocks
    • 你错过了承诺的意义 - domenic.me

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


    注意: 我已将此答案标记为社区 Wiki。因此,任何拥有至少 100 个声誉的人都可以编辑和改进它!如果您愿意,请随意改进此答案或提交一个全新的答案。

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

  • 在您的最后一个例子中,使用匿名函数有什么特殊原因吗?或者使用命名函数是否可以达到相同的效果?

  • 代码示例有点奇怪,因为您在调用函数后声明它。当然,这是因为提升而起作用,但这是故意的吗?

  • 这是死锁吗?felix kling 指着你的答案,而你指着 felix 的答案

  • 引用 10

    您需要了解,红色圆圈代码之所以是异步的,只是因为它是由 NATIVE 异步 javascript 函数执行的。这是您的 javascript 引擎的一个功能——无论是 Node.js 还是浏览器。它是异步的,因为它作为 \'回调\' 传递给本质上是黑盒的函数(用 C 等实现)。对于倒霉的开发人员来说,它们是异步的……只是因为。如果您想编写自己的异步函数,您必须通过将其发送到 SetTimeout(myfunc,0) 来破解它。你应该这样做吗?另一个争论……可能不是。

  • Fabrício 的回答非常正确;但我想用一些不太技术性的东西来补充他的回答,重点是通过类比来帮助解释异步性的概念


    打个比方……

    昨天,我工作中需要一位同事提供一些信息。我给他打了电话;以下是通话内容:

    :嗨,鲍勃,我想知道我们酒吧。吉姆想要一份报告,而你是唯一一个知道细节的人。

    鲍勃 :当然可以,但是要花大约 30 分钟的时间吧?

    :太好了,鲍勃。等你了解情况后再给我打电话吧!

    这时,我挂断了电话。由于我需要 Bob 提供的信息来完成我的报告,所以我放下报告去喝了杯咖啡,然后查看了一些电子邮件。40 分钟后(Bob 很慢),Bob 回电并给了我所需的信息。此时,我又继续写报告,因为我已经掌握了所需的所有信息。


    想象一下如果对话是这样的;

    :嗨,鲍勃,我想知道我们酒吧。吉姆想要一份报告,而你是唯一一个知道细节的人。

    鲍勃 :当然可以,但是要花大约 30 分钟的时间吧?

    :太好了,鲍勃。我会等的。

    我坐在那里等着。等了又等。等了 40 分钟。什么也没做,只是等待。最后,鲍勃给了我信息,我们挂了电话,我完成了报告。但我损失了 40 分钟的生产力。


    这是异步与同步行为

    这正是我们问题中所有示例中的情况。加载图像、从磁盘加载文件以及通过 AJAX 请求页面都是缓慢的操作(在现代计算环境中)。

    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) code。因此,我们不再需要将 outerScopeVar 变量声明为全局变量。

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

    总是 看到回调被指定为一个函数,因为这是 JavaScript 中定义某些代码但稍后才执行的唯一方法。

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

    * Technically you can use eval() as well, but eval()对于此目的来说是邪恶的 for this purpose


    如何让呼叫者等待?

    您目前可能有一些类似于此的代码;

    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 )。

  • 但是,如果您想在不同的计算中使用结果,或者将其存储在对象变量中,那么警报或发送到控制台有什么用呢?

  • 最后的代码示例展示了另一个参数的用法 - 此处为 src - 它与回调函数无关,甚至在回调后插入代码中。正如 Mastermind 所写,简洁明了!

  • 我怎么知道 img.onload=function(){.. 是异步的,我也许可以将其重写为 function img.onload(){.. 虽然这是不可能的,但它似乎是同步的。这是否在于人们在阅读了大量这些废话之后应该得到的 JS 感受?

  • 对于那些寻求快速参考以及使用承诺和异步/等待的一些示例的人来说,这是一个更简洁的答案。

    从简单的方法(不起作用)开始,该方法调用异步方法(在本例中 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);
    });
    

    承诺

    Promises 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合起来以协调多个异步操作。Node.js 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 通过其 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 函数 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();
    
  • 您的 Promises 示例基本上就是我过去几个小时一直在寻找的。您的示例非常漂亮,同时还解释了 Promises。为什么其他地方没有这个示例,真是令人困惑。

  • 这一切都很好,但如果您需要使用参数调用 getMessage() 怎么办?在这种情况下,您将如何编写上述代码?

  • 引用 18

    @Chiwda 您只需将回调参数放在最后:function getMessage(param1,param2,callback){...}。

  • 由于我正在运行旧的软件包,因此您提供的 $.Deferred() 实现正是我在 jQuery 中寻找的,谢谢。

  • 显而易见的是,杯子代表着 outerScopeVar .

    异步函数就像......

    asynchronous call for coffee

返回
作者最近主题: