基础环节

letconst是ES6中新增加的重要关键字,这两个关键字比起var最明显的区别就是,letconst都拥有块级作用域(在一对{}内形成的作用域),且没有变量提升(必须先声明在使用)。

  • var 只存在函数作用域和全局作用域(没有块级作用域的概念)
{
  let a = 123;
  var b = 123;
}
//读取不到a,能读取b
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) //123

let关键字

let不能重复声明赋值,var可以

let a = 3;
let a;  //Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)

let常用与for循环中

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

对比var

for(var i = 0; i < 5; i++){   //同步
//setTimeout可以看做一个异步的过程,在JavaScript的事件循环中,先执行同步代码,再执行异步代码,当异步代码执行时,for循环已经循环完毕,又因为在for循环中,由于用var定义变量,在循环体中执行的是同一个i,所以会输出5个5
  setTimeout(()=>{   //异步             
        console.log(i)
    },100);
}
// 5 5 5 5 5

但是如果用var要实现同一效果:

var _loop = function _loop(i) {
  setTimeout(function() {
    console.log(i);
  }, 100);
};

for (var i = 0; i < 5; i++) {
  _loop(i);
}

var*实现同一效果的原理就是要使用更多的闭包,而用***let**实现并没有形成闭包而只是使用了块级作用域,使用过多闭包会形成内存泄漏!

因为let是局部变量,所以可以在不同代码块(作用域)中重复声明

let a = 1;
{
  let a = 2;
  console.log(a); // 2
}
console.log(a);  // 1

不存在变量提升(有待商榷)

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错Uncaught ReferenceError: bar is not defined
let bar = 2;

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区(TDZ)

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

var tmp = 123;

if (true) {
  tmp = 'abc'; // Uncaught ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

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

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

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

块级作用域

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。

为什么需要块级作用域?

var的使用只有全局作用域和函数作用域,没有块级作用域,容易导致变量泄漏,这会带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量。

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

const关键字

const相当于只读变量,在声明创建变量的同时必须初始化

const PI = 3.1415926
PI; //3.1415926

const a;//Wrong!!会报错:Missing initializer in const declaration(常量声明中缺少初始值设定项)

const一旦声明初始化后不能修改,只读不写

const a = 3;
const a = 4;//Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)

constlet一样拥有块级作用域,但是与let不同,即使在不同的代码块中,也不能重复声明

const a = 1;
{
  const a = 2;
    a; 
}
a; 
//Wrong!!会报错:Identifier 'a' has already been declared(标识符a已声明)

本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据——>引用值(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // Uncaught TypeError: Assignment to constant variable.

为什么要进行变量提升?

变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象和变量。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。

首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

  • 在解析阶段

    ,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,this,arguments
  • 在执行阶段,就是按照代码的顺序依次执行。

那为什么会进行变量提升呢?主要有以下两个原因:

  • 提高性能
  • 容错性更好

提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;
var a;
console.log(a);

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:

var tmp = new Date();
function fn(){
  console.log(tmp);
  if(false){
    var tmp = 'hello world';
  }
}
fn();  // undefined

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

重谈Let和Const

细化知识

1、首先明确一点:提升不是一个技术名词。

要搞清楚提升的本质,需要理解 JS 变量的「创建create、初始化initialize 和赋值assign」,一个变量在使用之前必须经历这三步,有的地方把创建说成是声明(declare),有的地方把初始化叫做绑定(binding)

在回看之前的知识认知:

var的变量提升:函数执行在预编译过程中,AO(函数作用域)的产生时,变量会提升到AO当中,先创建函数执行时需要用到的变量并初始化为undefined

function的变量提升:在预编译过程中,AO的产生时,函数变量也会提升到AO当中,先创建函数名的变量并初始化为undefined,在把函数体赋值进去

2、在用let声明for循环变量——>for(let i = 0; i < 5; i ++ )中:

for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域

for( let i = 0; i< 5; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。

所以这跟for(var i =0 ; i < 5; i++)不同在于,var的变量在for循环开始到执行完毕都是同一个i,而let的变量在for循环中包括循环5次中,一共有6个不同的i(同名)

颠覆认知

提出观点

let我认为是有变量提升的

甚至说所有的声明(var, let, const, function, function*, class)都会进行提升(hoisted),这一观点是来自 StackOverflow的一个高票回答中的(冤有头债有主系列…)

最终把我说服的是这一个例子

let a = 1
{
  a = 2
  let a //Uncaught ReferenceError: Cannot access 'a' before initialization
}

证明观点

遇事不决反证法!

假如let没有变量提升的话,上面的a = 2会隐式变成 var a = 2,对a进行创建并提升到全局变量上,将let a = 1覆盖掉,这时全局变量a的值因为var a = 2 的变量提升变成2,但是结果是运行发现 a = 2 报错:未捕获的引用错误:在初始化之前无法访问“a”

上面的图可以体现出来,let a语句的确对上面的变量创建造成了影响

浏览器也对错误提示已经有了优化,也明确指出了变量创建的初始化环节之前导致无法访问变量,以前的浏览器提醒是:Uncaught ReferenceError: a is not defined

这说明上面的代码近似近似近似近似近似近似地可以理解为:(注意看注释中的 TDZ)

let a = 1
{
  let a // TDZ 开始的地方就是这里(let创建变量的‘create’环节提升)
  'start a TDZ'
  a = 2 // 由于 a = 2 在 TDZ 中,所以报错
  a // TDZ 结束的地方就是这里(let创建变量的‘init’环节)
  'end a TDZ'
}

陈述观点

我认为let也是有变量提升的,只是let是提升了变量创建三个流程(create创建,initialize初始化,assign赋值)中的第一项,而var是将前两项进行了提升

所以直接说let 和const没有变量提升我认为这个说法并不完全正确。