你可能不是真的懂let和const

252 阅读7分钟

在ES5我们习惯使用var,而var常常会给我们带来一些困扰,比如存在变量提升、允许重复命名(且后执行的会覆盖前执行的)、没有暂时性死区、没有块级作用域 。而ES6新增的let命令const命令,解决了这些困扰。

let

下面通过代码的方式来解答,变量提升、重复命名、暂时性死区、块级作用域,这些问题的解决和功能添加让平时开发的我们得到什么帮助。

  • 不存在变量提升
console.log(a); // undefined
var a = 1;

// 实际运行是这样的
var a;
console.log(a); // undefined
a = 1;

这样的代码,应该都见过吧?这就是变量提升。因为这种奇怪的行为其实代码是很难维护的。所以let解决了这样的问题。

console.log(a);
let a = 1; // a is not defined

let不存在var那样的情况。当然要是直接使用let定义变量不赋值,使用的时候值当然也是undefined

  • 不允许重复声明

使用var重复声明变量应该都是家常便饭了,但是let不允许在相同作用域内,重复声明同一个变量。

{
  var num = 123;
  console.log(num); // 123 
  var num = 456;
  console.log(num); // 456
}

{
  let num = 123;
  console.log(num); // 123
  let num = 456; // SyntaxError: Identifier 'num' has already been declared
}

输出的结果大家应该都能明白,但是为什么上面加了大括号呢?

因为v8引擎做了一些特殊的处理 这里let,所以let在控制台中不加上大括号可以重复声明。

  • 暂时性死区

只要块级作用域存在let命令,他所声明的变量就“绑定“这个区域,不再受外部的影响。

var num = 123;

{
  num = 456;  // ReferenceError: Cannot access 'b' before initialization
  let num;
}

什么意思呢?就是使用let命令声明变量之前,该变量不可用。

还有一些比较隐蔽的错误,但是在开发中经常会遇到

function test(x = y, y = 2){
  return (x, y);
}

test(); // ReferenceError: Cannot access 'b' before initialization

这也是暂时性死区的好处,避免开发时的坏习惯,一定在变量声明后才能使用,否则报错。

  • 块级作用域

内层变量可能会覆盖外层变量。

var num = 123;
function fun(){
  console.log(num);
  if(false){
    var num = 456;
    console.log(num);
  }
}

fun(); // undefined


// 代码实际运行
funcion fun(){
  var num;
  console.log(num);
  if(false) {
    num = 456;
    console.log(num);
  }
}
var num;
num = 123;

fun(); // undefined

这段代码只是想在函数中打印外层的num,在if判断内打印出自己定义的num,可是打印出来的却是undefined,这是因为变量提升导致的,同时也是因为没有块级作用域。

没有块级作用域还会导致循环变量泄露为全局变量。

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

块级作用域不会影响全局作用域的好处。

{
  var a = 1;
  let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not definedat <anonymous>:6:13

为什么a能正常打印b却会报错?因为用var定义的变量不存在块级作用域也就是全局都能访问,只有在函数中使用var才存在作用域不影响全局,因此ES5中常常会听到函数作用域,但是使用let完全不需要担心,只要是大括号包住那么JavaScript就认为他存在块级作用域因此可以避免影响全局作用域。

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

为什么a[6]()会打印出10呢?

因为i是用var声明的,上面也说了,用var声明的变量是全局的,所以全局都能访问,每一次循环,变量i的值都在改变,且循环体内给数组赋值的函数内的i是全局的,所以输出的值是10。可能有的人还没明白或者会说这个应用场景是什么。那再来看看下面的例子

// var定义i
for (var i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000)  // 5 5 5 5
}

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

这样有应用场景了吧。我希望控制他过多久输出,可是为什么是打印4个5呢?这里涉及的知识点比较多(闭包,提升,事件循环),今天讲的是let就不要偏题了,后面我会单独讲解,拿这个出来讲是因为let恰恰能解决这个问题。

因为用let声明的变量,不是全局范围有效的,因此当前的i只在本轮循环中有效,所以每一次循环其实都是一个新值,所以他们互不干扰。可能还有人会问每次都是新值,那怎么知道当前循环的值呢?这就是JavaScript引擎内部做的事情了,现在只要知道有这件事即可,后面会单独讲解,毕竟涉及的知识点是比较多的,不要跑题了。

  • 块级作用域声明函数
function fun(){ console.log('outside') }

(function (){
  if(false) {
    function fun(){ console.log('inside') }
  }
  fun();
}());

// ES5的实际运行
(funciton (){
  function fun(){ console.log('inside') }
  if(false){}
  fun(); // inside
}());

先说结果,ES5中当然打印出来的是insideES6中会提示fun is not a function 。应该都没问题。

但是这实际说明了什么?应该有些人没明白,那我来解释一下。先看看这个

if(true){
  function fun(){}
}

ES5中规定了,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域中声明。

后来ES6 引入了块级作用域,也明确了允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。可是需要兼容旧代码,所以附录B 里面也说明了浏览器可以不遵循上面的规则,有自己的行为方式。也就是

  • 允许在块级作用域内声明函数
  • 但是运行时会类似于var,也就是会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

需要注意:上面的规则只在ES6环境中实现有效,其他环境不需要遵守。

根据上面的规则再来看看ES6实际运行的效果

function fun() { console.log('outside') }
(function () {
  var fun = undefined;  // 类似于var 这是函数作用域
  if(false) {
    let fun1 = function () { console.log('inside') } // 块作用域
    fun = fun1;
  }
  fun(); // fun is not a function
}())

也就是说,在块级作用域中允许声明函数,但是又因为块级作用域的原因,且要兼容旧代码,所以块级作用域中的fun,没有被提升到外部,而提升到块级作用域的头部,这样应该都明白了吧。

const

const是什么?const声明的是一个常量,声明时必须同时赋值,且值不可变。

// const声明的常量值不可变
const P = 3.14159;
P = 3; // TypeError: Assignment to constant variable.

// const声明常量时须赋值
const num; // SyntaxError: Missing initializer in const declaration

constlet 一样存在块级作用域、暂时性死区

// 暂时性死区
console.log(P); // ReferenceError: Cannot access 'P' before initialization
const P = 3.14159;

// 块级作用域
{
  const P = 3.14;
}
console.log(P); // ReferenceError: P is not defined

其实const本质是保证变量所指向的内存地址不变,也就是说,引用类型中的属性值是可变,可添加的。

const obj = {};
obj.name = '公众号:前端树洞';
console.log(obj); // { name:'公众号:前端树洞' }

obj.name = '欢迎关注公众号:前端树洞';
console.log(obj); // { name:'欢迎关注公众号:前端树洞' }

const arr = [1, 2, 3];
arr.splice(1);
console.log(arr); // [1]

arr.push(4);
console.log(arr); // [1, 4]
  • 全局(window)

最后再来看看上面经常提起的全局到底说了什么。

全局作用域也叫做顶层对象,在浏览器中是window对象,在nodeJS中是global对象。前面经常会提到var定义的变量是全局可以访问的。先看代码

var num = 123;
console.log(window.num); // 123

function fn(){ 
  console.log(123123);  
}
window.fn(); // 123123

let num2 = 456;
console.log(window.num2); // undefined

const num3 = 789;
console.log(window.num3); // undefined

class fn{}
window.fn; // undefined

顶层对象与全局属性挂钩了。什么意思呢?

就是使用var命令和function命令声明的都是顶层对象的属性。所以能通过window找到对应的属性。ES6改变了这一点,但又为了保持兼容性,因此现在varfunction声明的变量和函数依然属于顶层对象的属性,但是letconstclass声明的全局变量,不属于顶层对象的属性。