使用 “let” 和“var”有什么区别?

ECMAScript 6 引入let语句

我听说它被描述为 “局部” 变量,但是我仍然不太确定它的行为与var关键字有何不同。

有什么区别?什么时候应该let在使用var

答案

范围规则

主要区别在于范围规则。 var关键字声明的变量的作用域范围是立即函数主体(因此,函数作用域),而let变量的作用域范围是由{ }表示的直接封闭块(因此,块作用域)。

function run() {
  var foo = "Foo";
  let bar = "Bar";

  console.log(foo, bar);

  {
    let baz = "Bazz";
    console.log(baz);
  }

  console.log(baz); // ReferenceError
}

run();

let关键字引入该语言的原因是函数范围令人困惑,并且是 JavaScript 中错误的主要来源之一。

另一个 stackoverflow 问题看这个示例:

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

My value: 3每次funcs[j]();都输出到控制台My value: 3 funcs[j]();由于匿名函数绑定到同一变量,因此被调用。

人们必须创建立即调用的函数以从循环中捕获正确的值,但这也很麻烦。

吊装

尽管使用var关键字声明的变量被 “提升” 到块的顶部,这意味着即使在声明它们之前,也可以在其封闭范围内访问它们:

function run() {
  console.log(foo); // undefined
  var foo = "Foo";
  console.log(foo); // Foo
}

run();

在评估其定义之前,不初始化 let 变量。在初始化之前访问它们会导致ReferenceError 。从块的开始到初始化处理之前,变量都处于 “临时死区”。

function checkHoisting() {
  console.log(foo); // ReferenceError
  let foo = "Foo";
  console.log(foo); // Foo
}

checkHoisting();

创建全局对象属性

在顶层, letvar不同,不会在全局对象上创建属性:

var foo = "Foo";  // globally scoped
let bar = "Bar"; // globally scoped

console.log(window.foo); // Foo
console.log(window.bar); // undefined

重新声明

在严格模式下, var使您可以在同一范围内重新声明相同的变量,而let引发 SyntaxError。

'use strict';
var foo = "foo1";
var foo = "foo2"; // No problem, 'foo' is replaced.

let bar = "bar1";
let bar = "bar2"; // SyntaxError: Identifier 'bar' has already been declared

let也可以用来避免关闭问题。它将绑定新的值,而不是保留旧的参考,如下面的示例所示。

for(var i=1; i<6; i++) {
  $("#div" + i).click(function () { console.log(i); });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>Clicking on each number will log to console:</p> 
<div id="div1">1</div>
<div id="div2">2</div>
<div id="div3">3</div>
<div id="div4">4</div>
<div id="div5">5</div>

上面的代码演示了经典的 JavaScript 关闭问题。对i变量的引用存储在单击处理程序闭包中,而不是i的实际值中。

每个单击处理程序都将引用同一对象,因为只有一个计数器对象包含 6 个,因此每次单击将获得 6 个。

一个通用的解决方法是将其包装在一个匿名函数中,并将i作为参数传递。现在也可以通过使用let代替var来避免此类问题,如下面的代码所示。

(在 Chrome 和 Firefox 50 中测试)

for(let i=1; i<6; i++) {
  $("#div" + i).click(function () { console.log(i); });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>Clicking on each number will log to console:</p> 
<div id="div1">1</div>
<div id="div2">2</div>
<div id="div3">3</div>
<div id="div4">4</div>
<div id="div5">5</div>

letvar什么区别?

  • 从函数开始,在整个定义函数中都知道使用var语句定义的变量。 (*)
  • 从使用let语句定义的变量开始就一直在其定义的块中知道该变量。 (**)

要了解差异,请考虑以下代码:

// i IS NOT known here
// j IS NOT known here
// k IS known here, but undefined
// l IS NOT known here

function loop(arr) {
    // i IS known here, but undefined
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( var i = 0; i < arr.length; i++ ) {
        // i IS known here, and has a value
        // j IS NOT known here
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( let j = 0; j < arr.length; j++ ) {
        // i IS known here, and has a value
        // j IS known here, and has a value
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here
}

loop([1,2,3,4]);

for( var k = 0; k < arr.length; k++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS NOT known here
};

for( let l = 0; l < arr.length; l++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS known here, and has a value
};

loop([1,2,3,4]);

// i IS NOT known here
// j IS NOT known here
// k IS known here, and has a value
// l IS NOT known here

在这里,我们可以看到我们的变量j仅在第一个 for 循环中已知,而在之前和之后都不知道。但是,我们的变量i在整个函数中是已知的。

另外,请考虑在声明块范围变量之前不知道它们,因为它们没有被提升。您也不允许在同一块中重新声明相同的块范围变量。这使得块范围的变量比全局变量或功能范围的变量更不容易出错,全局变量或功能范围的变量被提升并且在有多个声明的情况下不会产生任何错误。


今天使用let安全吗?

有人会说,将来我们只会使用 let 语句,而 var 语句将变得过时。 JavaScript 专家凯尔 • 辛普森Kyle Simpson)了一篇非常详细的文章,阐述了为什么他认为情况并非如此

但是,今天绝对不是这种情况。实际上,我们实际上需要自问,使用let语句是否安全。该问题的答案取决于您的环境:

  • 如果您正在编写服务器端 JavaScript 代码( Node.js ),则可以安全地使用let语句。

  • 如果您正在编写客户端 JavaScript 代码并使用基于浏览器的编译器(例如Traceurbabel-standalone ),则可以安全地使用let语句,但是就性能而言,代码可能不是最佳选择。

  • 如果您正在编写客户端 JavaScript 代码并使用基于 Node 的编译器(例如traceur shell 脚本Babel ),则可以安全地使用let语句。并且由于您的浏览器仅会知道已转译的代码,因此应限制性能方面的弊端。

  • 如果您正在编写客户端 JavaScript 代码并且不使用翻译器,则需要考虑浏览器支持。

    仍然有一些根本不支持let浏览器:

在此处输入图片说明


如何跟踪浏览器支持

有关在阅读此答案时哪些浏览器支持let语句的最新概述,请参见Can I Use页面


(*)因为提升了 JavaScript 变量,所以可以在声明它们之前初始化和使用全局范围和功能范围的变量。这意味着声明始终在作用域的顶部。

(**)不提升块范围的变量

下面是一些示例let关键字说明。

let工作非常像var 。主要区别在于var变量的范围是整个封闭函数

Wikipedia 上的此表显示哪些浏览器支持 Javascript 1.7。

请注意,只有 Mozilla 和 Chrome 浏览器支持它。 IE,Safari 和其他可能没有的。

可接受的答案遗漏了一点:

{
  let a = 123;
};

console.log(a); // ReferenceError: a is not defined

let

块范围

使用let关键字声明的变量是块作用域的,这意味着它们仅在声明它们的中可用。

在顶层(功能外部)

在顶层,使用let声明的变量不会在全局对象上创建属性。

var globalVariable = 42;
let blockScopedVariable = 43;

console.log(globalVariable); // 42
console.log(blockScopedVariable); // 43

console.log(this.globalVariable); // 42
console.log(this.blockScopedVariable); // undefined

函数内部

在函数内部(但在块外部), let具有与var相同的作用域。

(() => {
  var functionScopedVariable = 42;
  let blockScopedVariable = 43;

  console.log(functionScopedVariable); // 42
  console.log(blockScopedVariable); // 43
})();

console.log(functionScopedVariable); // ReferenceError: functionScopedVariable is not defined
console.log(blockScopedVariable); // ReferenceError: blockScopedVariable is not defined

块内

在块内使用let声明的变量不能在该块外访问。

{
  var globalVariable = 42;
  let blockScopedVariable = 43;
  console.log(globalVariable); // 42
  console.log(blockScopedVariable); // 43
}

console.log(globalVariable); // 42
console.log(blockScopedVariable); // ReferenceError: blockScopedVariable is not defined

循环内

let in 循环声明的变量只能在该循环内部引用。

for (var i = 0; i < 3; i++) {
  var j = i * 2;
}
console.log(i); // 3
console.log(j); // 4

for (let k = 0; k < 3; k++) {
  let l = k * 2;
}
console.log(typeof k); // undefined
console.log(typeof l); // undefined
// Trying to do console.log(k) or console.log(l) here would throw a ReferenceError.

带闭环

如果在循环中使用let而不是var ,则每次迭代都会获得一个新变量。这意味着您可以安全地在循环内使用闭包。

// Logs 3 thrice, not what we meant.
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

// Logs 0, 1 and 2, as expected.
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}

时间死区

由于存在临时死区 ,因此在声明之前无法访问使用let声明的变量。尝试这样做会引发错误。

console.log(noTDZ); // undefined
var noTDZ = 43;
console.log(hasTDZ); // ReferenceError: hasTDZ is not defined
let hasTDZ = 42;

无需重新声明

您不能使用let多次声明相同的变量。您也不能使用let声明变量,该变量具有与另一个使用var声明的变量相同的标识符。

var a;
var a; // Works fine.

let b;
let b; // SyntaxError: Identifier 'b' has already been declared

var c;
let c; // SyntaxError: Identifier 'c' has already been declared

const

constlet非常相似,它具有块作用域并具有 TDZ。但是,有两件事是不同的。

无需重新分配

使用const声明的变量无法重新分配。

const a = 42;
a = 43; // TypeError: Assignment to constant variable.

请注意,这并不意味着该值是不可变的。它的属性仍然可以更改。

const obj = {};
obj.a = 42;
console.log(obj.a); // 42

如果要拥有一个不变的对象,则应使用Object.freeze()

需要初始化

使用const声明变量时,必须始终指定一个值。

const a; // SyntaxError: Missing initializer in const declaration

这是两者之间差异的示例(刚刚开始支持 chrome):
在此处输入图片说明

如您所见, var j变量的值仍在 for 循环范围(Block Scope)之外,但在 for 循环范围之外, let i变量未定义。

"use strict";
console.log("var:");
for (var j = 0; j < 2; j++) {
  console.log(j);
}

console.log(j);

console.log("let:");
for (let i = 0; i < 2; i++) {
  console.log(i);
}

console.log(i);

有一些细微的差异 - let范围界定的行为更像变量范围界定的行为,或多或少与任何其他语言一样。

例如,它的作用域为封闭的块,它们在声明之前不存在,等等。

但是,值得注意的是, let只是较新的 Javascript 实现的一部分,并且具有不同程度的浏览器支持

主要区别在于范围的区别,而let只能在声明的范围内使用,例如在 for 循环中,例如var可以在循环外部访问。从MDN 中的文档中(也有 MDN 的示例):

let允许您声明范围仅限于使用它的块,语句或表达式的变量。这与var关键字不同,该关键字在全局范围内或在整个函数本地定义变量,而不管块范围如何。

let声明的变量的范围是定义它们的块以及任何包含的子块。这样, 让我们的工作非常像var 。主要区别在于var变量的范围是整个封闭函数:

function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // same variable!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // different variable
    console.log(x);  // 2
  }
  console.log(x);  // 1
}`

在程序和函数的顶层, letvar不同,不会在全局对象上创建属性。例如:

var x = 'global';
let y = 'global';
console.log(this.x); // "global"
console.log(this.y); // undefined

在块内使用时,让该变量的作用域限制在该块内。请注意var之间的区别, var的范围在声明它的函数内部。

var a = 1;
var b = 2;

if (a === 1) {
  var a = 11; // the scope is global
  let b = 22; // the scope is inside the if-block

  console.log(a);  // 11
  console.log(b);  // 22
} 

console.log(a); // 11
console.log(b); // 2

另外,别忘了它的 ECMA6 功能,因此尚未得到完全支持,因此最好始终使用 Babel 等将其转换为 ECMA5。有关访问babel 网站的更多信息

  • 可变不吊装

    let 不会提升它们出现在其中的块的整个范围。相反, var可以如下提升。

    {
       console.log(cc); // undefined. Caused by hoisting
       var cc = 23;
    }
    
    {
       console.log(bb); // ReferenceError: bb is not defined
       let bb = 23;
    }

    实际上,Per @Bergi, 都吊起了varlet

  • 垃圾收集

    let块范围与关闭和垃圾回收有关,可以回收内存。考虑,

    function process(data) {
        //...
    }
    
    var hugeData = { .. };
    
    process(hugeData);
    
    var btn = document.getElementById("mybutton");
    btn.addEventListener( "click", function click(evt){
        //....
    });

    click处理程序回调完全不需要hugeData变量。从理论上讲,在process(..)运行之后,可以对巨大的数据结构hugeData进行垃圾回收。但是,某些 JS 引擎仍可能必须保留这种庞大的结构,因为click函数在整个作用域内都是封闭的。

    但是,块作用域可以使这种庞大的数据结构被垃圾回收。

    function process(data) {
        //...
    }
    
    { // anything declared inside this block can be garbage collected
        let hugeData = { .. };
        process(hugeData);
    }
    
    var btn = document.getElementById("mybutton");
    btn.addEventListener( "click", function click(evt){
        //....
    });
  • let循环

    let in the loop 可以将其重新绑定到循环的每次迭代,并确保从上一次循环迭代结束时重新为其分配值。考虑,

    // print '5' 5 times
    for (var i = 0; i < 5; ++i) {
        setTimeout(function () {
            console.log(i);
        }, 1000);  
    }

    但是,用let替换var

    // print 1, 2, 3, 4, 5. now
    for (let i = 0; i < 5; ++i) {
        setTimeout(function () {
            console.log(i);
        }, 1000);  
    }

    因为let用 a)初始化程序表达式 b)每次迭代(以前是评估增量表达式)的名称创建一个新的词法环境,所以这里有更多详细信息。