【问题标题】:How does Javascript's Scope work, getting unexpected behavior?Javascript 的 Scope 是如何工作的,出现意外行为?
【发布时间】:2014-01-08 08:43:03
【问题描述】:

这是一段代码示例,类似于让我悲伤的代码:

for(var i=0;i<3;i++){
    var Proof = true
    if (i == 0){
        Proof = false
        //var CallBack = function(){alert(Proof);}; // true????
        var CallBack = function(){alert(i);};
    }
}

// Whatever happened to local scope? shouldn't CallBack be undefined now?
CallBack()// alert(3), should be 0?

CallBack 不是未定义的,它会发出 true 或 3 警报,有人可以向我解释一下是怎么回事让它像这样吗?是否有一些我缺少的本地关键字,或者 JS 没有范围?我没有使用任何框架,希望能得到一些帮助来解决这个问题,并正确地做这种事情......

【问题讨论】:

    标签: javascript scope


    【解决方案1】:

    JavaScript 根本没有块作用域(但是,见下文)。所有变量都在它们出现的整个函数中声明(或在全局范围内,如果它们在全局范围内)。

    假设您引用的代码是其范围内的唯一代码,它完全相同与此相同:

    var i;
    var Proof;
    var Callback;
    
    for(i=0;i<3;i++){
        Proof = true
        if (i == 0){
            Proof = false
            //CallBack = function(){alert(Proof);}; // true????
            CallBack = function(){alert(i);};
        }
    }
    
    // Whatever happened to local scope? shouldn't CallBack be undefined now?
    CallBack()// alert(3), should be 0?
    

    更多(在我的博客上)Poor, misunderstood var


    CallBack()// alert(3), should be 0?

    不,3 是正确的,因为这是 i 在调用 CallBack 时的值。 CallBack 是对创建它的上下文的闭包。它具有对上下文(包括i 变量)的持久引用,而不是创建时存在的内容的副本。

    如果您想获得0,则必须让CallBack 关闭i 以外的其他内容,这不会改变。执行此操作的典型方法是使用您将值传递到的构建器函数:

    function buildCallBack(index) {
        return function() { alert(index); };
    }
    

    然后:

    CallBack = buildCallBack(i);
    

    i 的值作为参数index 传递给buildCallBackbuildCallBack 创建一个函数,关闭该调用的上下文 buildCallBack 并使用该 index 参数。由于该参数的值永远不会更改,因此回调警报0(在这种情况下)而不是3

    更多(在我的博客上)Closures are not complicated


    我上面说“还”的原因是,下一版本的规范(ES6)将为新的letconst 关键字和块函数声明引入新的block-scope semanticsvar 将保持不变,但如果您使用 let 来声明变量,则它具有块范围而不是函数/全局范围。

    【讨论】:

    • 对于回调的堆栈,至少不应该通过副本应用吗?或者只是通过副本传递直接变量赋值,这也有点可怕,我已经编写了超过舒适数量的 Js 代码而没有理解这一点
    • @JohnDoe:什么通过副本?问题似乎与范围有关,而不是函数调用。
    • 对不起,在 Lua(我的母语)中,本地函数的函数体中携带的变量在其自己的范围内声明(并且每个范围在到达时都有一个变量的“副本” ) 因此,即使稍后调用该函数,该函数的变量“副本”仍将处于活动状态
    • @JohnDoe:我明白了。我刚刚注意到代码中的注释询问为什么警报显示 3 而不是 0,所以我添加了答案来解决这个问题。
    • 感谢您解释缺乏范围 :-) 尽管 Zub 的回答似乎更有吸引力,但由于我的应用程序,我更重视语法清洁,而不是速度。
    【解决方案2】:

    当 CallBack 被调用时,for 循环已经完成。所以i 等于 3。

    如果你想让i 是本地的,你应该这样写:

    var CallBack;
    for(var i=0;i<3;i++){
        (function(index) {
            var Proof = true
            if (index == 0){
                Proof = false
                //var CallBack = function(){alert(Proof);}; // true????
                CallBack = function(){alert(index);};
            }
        })(i);
    }
    
    CallBack()
    

    这是demo

    更新

    这里有一个替代方案:

    var CallBack;
    for(var i=0;i<3;i++){
        var Proof = true
        if (i == 0){
            Proof = false
            CallBack = (function(index) {
                return function(){alert(index);};
            })(i);
        }
    }
    
    CallBack()
    

    demo

    更新 2

    解释:

    通过写(function(index) {...})(i),我们声明了一个匿名函数并立即调用它(我们也可以写function(index) {...}(i),但我认为第一种情况更清楚)。在 javascript 中,function 也是一种设置另一个变量范围的方法,因此在我们的例子中,我们“记住”了 index 变量中 i 变量的当前状态,该变量仅在我们的匿名函数中可见。

    当调用 'CallBack' 时,alert 显示 0,因为调用该函数时 i 等于 0

    【讨论】:

    • 通常最好让回调的构建器是一个单独的、命名的、可重用的函数,特别是在你循环的情况下。否则,除了清晰度问题之外,您还会无缘无故地反复创建和丢弃构建器函数。
    • 用线程破解它的有趣想法,但是为什么将函数包含在()中?以及您如何将 i 传递给它,看起来像是一个调用,但似乎不是 '(function{})(i)'?
    • @John Doe 通过写(function(i) {...})(i) 我声明函数并立即调用它。我通过ii 变量出现在函数体中(我可以用不同的方式命名它)。内部i 反映当前外部i 状态
    • @T.J. Crowder 使用 builder 是个好主意。没想到
    • @JohnDoe: "...function(){}() 对我来说似乎比...更清楚..." 只要您已经在表达式中(在您的情况下,您位于= 的右侧),您可以这样做,没问题。有时您需要那些外部括号来避免解析器将表达式视为函数 declaration; details here.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-06
    • 1970-01-01
    • 2016-01-06
    • 2018-02-13
    • 2021-01-20
    • 1970-01-01
    • 2016-04-30
    相关资源
    最近更新 更多