JavaScript变量是在循环外还是内部声明?

229

在AS3中,我相信你应该在循环外初始化所有变量以提高性能。在JavaScript中是否也是如此?哪个更好/更快/最佳实践?

var value = 0;

for (var i = 0; i < 100; i++)
{
    value = somearray[i];
}
或者
for (var i = 0 ; i < 100; i++)
{
    var value = somearray[i];
}

8
外面!永远在外面。 - BGerrissen
39
在JavaScript和AS3中,变量声明不是都会被提升到函数作用域吗?如果我没错的话,那么这并不重要。 - spender
3
@Andy - 你在函数体中尝试过先声明再赋值吗?也许你的先入为主的想法会让你误入歧途。关于性能,如果JS被解释执行,在循环块内会消耗更多的循环周期。但是,如果被编译(现在大多数引擎都这样做了),这就不重要了。 - spender
2
很好的问题!谢谢。阅读了所有答案后,我相信如果只是一个小循环或仅有一个临时变量,我会将它们保留在需要它们的地方,这不会影响性能。如果一个变量在函数内使用多次,为什么不在函数内引用它,最后全局变量可以放在fn()外面。 - Dean Meehan
3
我很惊讶没有人试图衡量性能。我创建了一个jsperf。对于Safari和Firefox,在循环内部声明变量似乎会更快一些,而对于Chrome则相反... - Buzut
显示剩余4条评论
13个回答

304

在 JavaScript 或 ActionScript 中,意义和性能完全相同。

var 是指导解析器的指令,并不是在运行时执行的命令。如果一个特定的标识符在函数体(*)中的任何位置被声明为 var 一次或多次,则块中对该标识符的所有使用都将引用局部变量。无论 value 是否在循环内、外或两者都声明为 var 都没有区别。

因此,您应该编写您认为最可读的方式。我不同意 Crockford 的观点,即将所有变量放在函数顶部总是最好的选择。对于在代码部分临时使用的变量,最好在该部分中声明var,以便该部分独立存在并可复制粘贴。否则,在重构期间将几行代码复制粘贴到新函数中时,没有单独挑选和移动相关的 var,您就会得到一个意外的全局变量。

尤其是:

for (var i; i<100; i++)
    do something;

for (var i; i<100; i++)
    do something else;
Crockford建议你删除第二个var(或者删除两个var并在上面加上var i;),但是jslint会对此进行抱怨。但我个人认为保留两个var更易于维护,将所有相关的代码放在一起,而不是在函数顶部有一个额外的容易被遗忘的代码块。
个人而言,在独立代码段中为变量的第一次赋值声明var,无论同一函数的其他部分是否有使用相同变量名的另外一个单独的用法。对我来说,在JS中声明var本身就是一种不良的设计(最好让变量默认为局部变量);我不认为我的职责是复制JavaScript中[旧版] ANSI C的限制。
(*:除了嵌套函数体内)

4
我仍然无法确定我是否支持Crockford的观点。在块内声明变量感觉有点像使用条件函数语句(这是不好的)...但是,我也同意你的观点 :) #困惑 - Daniel Vassallo
20
我不同意Crockford(在Daniel的回答中引用)的推论,作为JavaScript开发者,我们不应该编写代码以便让其他“C家族”程序员能够理解它。我经常在if块和循环中使用var,因为这对我来说更有意义。 - Andy E
4
OP在问循环体内的变量是否应该在循环开始之前声明。循环的索引值显然是一个特殊情况(并且被提升了),并没有帮助OP解决问题。 - mkoistinen
22
循环索引并不是一个特殊情况,它们的处理方式和普通赋值完全相同,并且会像普通赋值一样进行提升。 - bobince
31
Crockford 在这个问题上是错的(还有其他问题,但我岔开了)。要求 var 仅在函数顶部使用只会导致意外创建全局变量。而将一堆不相关的变量都声明在一个地方,在语义上是没有意义的,尤其是其中一些变量可能永远都不会被使用。 - MooGoo
显示剩余12条评论

66
在理论上,这在JavaScript中不应该有任何区别,因为该语言没有块作用域,只有函数作用域。
我不确定性能的论点,但Douglas Crockford仍然建议var语句应该是函数体中的第一条语句。引用Code Conventions for the JavaScript Programming Language的话来说:

JavaScript没有块作用域,因此在块中定义变量可能会让有其他C系列语言经验的程序员感到困惑。请在函数顶部定义所有变量。

我认为他说得有道理,正如您可以在以下示例中看到的那样。在函数顶部声明变量不应该让读者以为变量 for循环块的范围内:
function myFunction() {
  var i;    // the scope of the variables is very clear

  for (i = 0; i < 10; i++) {
    // ...
  }
}

不是循环内的变量声明会降低性能,而是静态值赋值(比如 len = list.length)。 - BGerrissen
这不是关于for循环的索引,而是关于循环内部发生的事情。 - mkoistinen
@mkoistinen:我认为我没有错过重点。我只引用了一个权威来源,该来源仍然建议将变量声明放在块外面,无论是否存在性能增益。我的回答并不是要回答性能问题(我强调了这一点),但我仍然认为它对问题的主题有价值...我认为这个答案不应该得到6个反对票(其中2个后来被撤回)。 - Daniel Vassallo
for(var i; ...) 只会让初学者和最迷茫的JS程序员感到困惑,因为循环有其自己的作用域... - bryc
5
ES6的let关键字对这个答案有什么影响? - jbyrd
正如Steve McConnell在《代码大全》中所讨论的那样,变量应该在使用它们的地方附近声明,不是为了作用域的原因,而是为了将相关的内容放在一起,以便于调试、重构和理解。如果变量在使用的地方附近声明,就不太可能出现过时的变量挂在那里的情况。 - johny why

60

ECMA-/Javascript语言会将任何在函数内声明的变量进行提升(hoist)到函数顶部。这是由于该语言具有函数作用域(function scope)而不像其他类C语言一样具有块级作用域(block scope)。这也被称为词法作用域(lexical scope)

如果你声明了如下代码:

var foo = function(){
    for(var i = 0; i < 10; i++){
    }
};

这个会被提升到:

var foo = function(){
    var i;
    for(i = 0; i < 10; i++){
    }
}

因此,在性能方面没有任何区别(但如果我在这里完全错误,请纠正我)。
相比于在函数顶部之外的其他位置声明变量,更好的理由是 可读性。在 for-loop 中声明变量可能会导致错误的假设,即该变量只能在循环体内访问,这是完全错误的。实际上,您可以在当前范围内的任何地方访问该变量。


12
ES6的let如何影响这个答案? - jbyrd
我同意jbyrd的观点。这真的很误导人,因为ES6中的let和const是块级作用域。 - Matt Strom

14

明年,所有的浏览器都将拥有JS引擎来预编译代码,因此性能差异(来自于反复解析相同的代码块和执行赋值操作)应该会变得可以忽略不计。

此外,除非必须,否则不要为了性能而优化。让变量保持靠近第一次需要它们的地方可以使您的代码更加清晰。但是,不习惯块作用域语言的人可能会感到困惑。


JavaScript程序员为什么要做这一点以避免让非JavaScript程序员感到困惑?非JavaScript程序员已经会因为很多其他事情而感到困惑了。 - johny why

9

另一个需要考虑的因素是,现在我们有了 ES2015 中的 letconst,你可以将变量的作用域限定在循环块内。因此,除非你需要在循环外使用相同的变量(或者如果每次迭代都依赖于上一次迭代对该变量进行的操作),否则最好采用以下方法:

for (let i = 0; i < 100; i++) {
    let value = somearray[i];
    //do something with `value`
}

1
在每次迭代中,我们不是重新声明了值吗?let 如何允许这样做? - Altair21
2
{}之间的部分是它自己的作用域;let value仅适用于该块(我想也适用于该特定迭代),并且在下一次迭代中不再存在。 - Matt Browne

4

我刚在Chrome浏览器中进行了一个简单的测试。请在您的浏览器中尝试此链接,并查看结果。

  var count = 100000000;
    var a = 0;
    console.log(new Date());

    for (var i=0; i<count; i++) {
      a = a + 1
    }

    console.log(new Date());

    var j;
    for (j=0; j<count; j++) {
      a = a + 1;
    }

    console.log(new Date());

    var j;
    for (j=0; j<count; j++) {
        var x;
        x = x + 1;
    }

    console.log(new Date());

结果是最后一个测试需要大约8秒钟,而前面的两个测试只需要大约2秒钟。非常可靠,而且无论顺序如何都是如此。因此,这向我证明,应该始终在循环外声明变量。对我来说,有趣的情况是第一个测试,在其中我在for()语句中声明了i。这个测试似乎和第二个测试一样快,其中我预先声明了索引。

14
@KP: 结果只有在你自己测试或大量人进行验证后才能被证明。 @mkoistinen:我构建了一个更公平的测试,http://jsfiddle.net/GM8nk/。在Chrome 5中多次运行脚本后,我发现没有一个一贯的赢家。三个变化在几次刷新后表现都比其他的更好。很抱歉,我投了反对票。请注意,您可能需要在其他浏览器中运行此代码(http://jsfiddle.net/GM8nk/1/)。 IE和Fx不支持100百万次迭代。 - Andy E
2
我的结果很不稳定,没有明显的跨浏览器获胜者,尽管有时速度差异很大。很奇怪。我认为安迪的小提琴测试更好,将每个候选项放在自己的函数中...当然,如果原始脚本在函数外运行,它实际上不应该测试任何东西,因为var声明了一个本来就是全局的变量。 - bobince
4
一年多过去了,但SHAPOW - sdleihssirhc
2
这不是我的,但我想你们中的一些人会感兴趣:http://jsperf.com/var-in-for-loop - m1.
-1:代码存在错误:“var a”未定义,因此“a = a + 1”是“undefined + 1”,结果为NaN。使用“var a = 0”,最后一个测试是最快的。https://jsfiddle.net/zzg75j5o/2/ - Thomas
显示剩余9条评论

1
JavaScript是一种由C或C++底层编写的语言,我不太确定是哪一个。它的目的之一是为了节省处理内存的麻烦。 即使在C或C++中,当变量在循环内部声明时,你也不必担心它是否会消耗大量资源。为什么在JavaScript中要担心呢?

1
C或C++可能位于JavaScript的底层。但我们不应忘记,浏览器将JavaScript转换为底层语言(C、C++)。因此性能取决于浏览器。 - Kira
3
它实际上并不会被转换为C/C++。Javascript会被编译成一组指令,由JS运行时(即在浏览器中运行的虚拟机)执行。对于其他动态语言,如Python和Ruby,也适用同样的原理。 - Anthony E

1
我希望将可读性和性能结合起来。因此,我的首选是在循环内部声明变量,这意味着我将具有块范围封装。
for (let i = 0, sum = 0; i < count; i++) { // count also be declared here like count = array.length;
  sum = sum + 1;
}

根据之前提供的 测试 性能 fiddle,获胜者是第4个


0

这取决于你想要实现什么... 如果value只是循环块内的临时变量,那么使用第二种形式会更清晰。它也更加合乎逻辑和详细。


2
我发现将所有变量声明(包括临时变量)都推到顶部可能会导致混乱,因为它会变得“嘈杂”。 - Daniel Sokolowski

0

在for循环内部或外部声明变量并没有什么区别。 以下是用于测试的示例代码。

function a() {
   console.log('Function a() starts');
   console.log(new Date());
    var j;
    for (j=0; j<100000000; j++) {
        var x;
        x = x + 1;
    }
    console.log(new Date());
    console.log('Function a() Ends');
}
a()
function b() {
console.log('Function B() starts');
   console.log(new Date());
    var a;
    var j;
    for (j=0; j<100000000; j++) {
      a = a + 1;
    }
    console.log(new Date());
    console.log('Function B() Ends');
}
b()

结果显示在我的情况下

Function a() starts
VM121:3 Thu Apr 12 2018 15:20:26 GMT+0530 (India Standard Time)
VM121:9 Thu Apr 12 2018 15:20:26 GMT+0530 (India Standard Time)
VM121:10 Function a() Ends
VM121:14 Function B() starts
VM121:15 Thu Apr 12 2018 15:20:26 GMT+0530 (India Standard Time)
VM121:21 Thu Apr 12 2018 15:20:26 GMT+0530 (India Standard Time)
VM121:22 Function B() Ends

谢谢--MyFavs.in


在这两种情况下,您都在循环外声明了j! X_x - john k
我在 Chromium 81 中尝试了使用 let 替换 var,发现 a() 的速度稍慢一些(大约是 120 毫秒 vs 115 毫秒 = 约 6% = 在我看来不重要)。 - mikiqex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接