18888628835/Blog

let、const与var的区别

18888628835 opened this issue · 0 comments

let、const与var的区别

let声明和var声明受块级作用域限制不同

如下代码

{
  let a=10;
  var b=20
}
a //Uncaught ReferenceError: a is not defined
b //20

当被块级作用域包围时,可以看到let声明使得块级作用域外部无法访问a这个变量,但是var声明的b却不会有这样的问题。

说明var不受块级作用域的影响,而let声明只在块级作用域中有效。

这就使得在循环时,let比var更有优势,因为不会出现变量外泄问题。

for(let i=0;i<5;i++){
  ...
}
console.log(i) //Uncaught ReferenceError: i is not defined

for(var i=0;i<5;i++){
  ...
}
console.log(i) //5

当for循环配合var声明时,由于不受块级作用域影响,全局只有一个作用域,i变量所在的作用域就是全局作用域,这就使得i变量会外泄。

当for循环配合let声明时,则会创建独立的块级作用域,与全局作用域不同,所以我们在外层无法获取到内部的i变量。

let声明配合循环可以创建独立的作用域

上面说到let声明配合循环会生成独立的作用域,下面有个更好的例子

for(var i=0;i<6;i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
//6 6 6 6 6 6

这个例子在《你不知道的JavaScript》中有提及,被我写到博客上多次。

由于这个循环只有一个全局作用域,所以当循环结束,异步代码执行时,只能获取到全局作用域上的i,这时候i已经变成了6,所以打出来就是6个6。

例子换成let就不一样了

for(let i=0;i<6;i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
//0 1 2 3 4 5 

这是由于每次循环都会生成一个作用域,当前循环中的i就存在于这个作用域中,所以实际上会生成6个不同的作用域,那么当异步代码执行时,会从对应的作用域读取i,所以结果就是0 1 2 3 4 5

再来看一个例子:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

由于只有一个作用域,所以i也只有一个。循环结束,i变成了10。

数组中的函数执行后console.log(i)中的i始终指向全局作用域下的i。所以最后的输出是10。

如果变成let声明,则会产生多个作用域,每个作用域下的i都是独立的,都是新生成的变量,当进行访问时,会访问函数生成时的那个作用域里的i,结果则是我们希望的。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

有一点需要注意的是,for循环会生成一个父级作用域,它还有一个子级作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

我们知道大括号{}会生成一个作用域,上面的例子中的let i=abc就是在大括号中生成的,它跟for循环作用域下产生的i又不同,它是在循环体内产生的单独的子作用域。

let声明没有提升

由于原来的js编译设计机制问题,导致var声明会被提升到代码顶部,所以我们在声明变量之前也可以访问变量。

console.log(a)//undefined
var a=1

个中原理可以看这篇博客【你不知道的JavaScript】作用域是什么?

不过es6纠正了这个逻辑,使用let就不存在变量提升问题

console.log(a)//Uncaught ReferenceError: a is not defined
let a=10

一定要先声明后访问,因为先访问时,变量a是不存在的,所以报了一个查找不到的错误。

let声明有暂时性死区问题

只要块级作用域内存在let命令,那么就会牢牢地被绑定在这个区域,不受外部影响。

var a=123
function fn(){
  console.log(a) //Uncaught ReferenceError: Cannot access 'a' before initialization
  let a=888
}

上面代码中,虽然全局作用域下有个a,但是引擎已经预先知道内部有个let声明的a了,所以不会往外部获取全局下的a,而是先把函数作用域内的a锁死。

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

有兴趣的同学还可以看看我写的这一篇JS薛定谔的变量提升

let声明不允许重复声明

在同一作用域下,let不允许声明两次同名的变量。

{
  let a=0
  let a=0
}
//Uncaught SyntaxError: Identifier 'a' has already been declared

不过var是可以的,新变量会覆盖老变量。

var a=0
var a=1
a //1

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

let声明不会将变量挂到window顶层对象中

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

全局下let声明跟var声明所在的作用域并不算同一层,var所在的全局作用域会指向window对象,所以我们可以通过window来访问var声明的全局变量,但是let却不能通过window对象访问。

var a=10
window.a //10

let b=99
window.b //undefined

这是因为es6新设计了全局变量,纠正了原来全局变量会与window顶层对象挂钩的设计问题。

const声明

const声明跟let声明一模一样,唯一的区别就是const声明是一个常量,一旦定义,不能改变。

所以不能只声明不赋值

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

赋值后,值不可改变。

const a=10
a=100 //Uncaught TypeError: Assignment to constant variable.

如果赋值给一个复杂对象,那么变量的值就是一个引用地址,只要不改变这个引用地址,还是可以修改复杂对象内部的属性的。

const a={name:'qiu'}
a.name='qiuyanxi' 
a //{name:"qiuyanxi"}
a={name:"qiuyanxi"}//报错