zf-fe

✍️ Tangxt ⏳ 2020-05-25 🏷️ ES6

08-let、var、const的区别

★为啥讲这个?

基于前边所讲的内容,还有一些特殊的情况需要处理,如面对用var和直接function声明一个变量的方式,会有提前声明的特殊性。

当然,就平时开发来说,我们很少会用var了,而 function xxx() {} 倒是经常在用

所以我们一般都不去考虑 var 声明的变量会有变量提升这件事儿……

既然你var特殊,那我就不用你,这样我就不用去考虑你的特殊性了

★变量提升

◇是什么?

有这样一段代码:

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

之前我们说到,要执行这代码会走怎么一个流程:

  1. ECStack
    1. EC(G)
      1. VO(G)
      2. 执行代码(由于我们都是把变量声明在第一行的,所以在代码执行的时候,我们直接就把变量扔到VO(G)里边前,并没有体现Hoisting的概念)

在当前EC代码执行之前:

  1. 把所有带var或者function关键的声明或定义给提上来(注意:带var的只是提前声明,而带function的则会提前的声明+定义

因此,上边代码的执行流程是这样的:

  1. ECStack
  2. EC(G)
  3. VO(G)
    1. a -> var a 提前声明了,但未定义,即为undefined
  4. 执行代码
    1. console.log(a)-> 看到VO(G)里的a是undefined值
    2. a = 12-> 改变VO(G)里的a的状态为12
    3. console.log(a)-> 12

function呢?-> 它更有特殊性

如:

console.log(a)
fn()
var a = 12
function fn() {
  console.log('hi')
}

执行过程:

  1. ECStack
  2. EC(G)
  3. VO(G)
    1. a -> undefined
    2. fn -> 0x101、fn[[scope]] = VO(G)
  4. 代码执行
    1. console.log(a)
    2. fn()
    3. ……

自打ES6出现后,我们都不怎么管变量提升的事儿了……

以下只是测试,平时不会写出现这样的代码:

提升的优先级

注意,同一个EC上,出现相同的变量,并不会重新声明,但会重新赋值

上图的另一种理解姿势(老师认为的是这种,嗯,我也认同):

这种理解姿势才是有道理的,不然浏览器难道先把所有代码都看一遍,即先把 var声明的变量给拎出来,然后再把function声明的变量给拎出来? -> 还是看一个声明一个比较有道理,遇到已经声明过的,就跳过,但function除了声明还有定义,所以就得重新定义一下声明过的变量

不管哪种姿势理解,对执行结果并没有影响!

弊端:不方便管控,不符合正常逻辑(正常逻辑是,代码一行行的走,而不是让代码插队提前的走)

★ES6 -> let、const

◇let vs var

1、区别一

console.log(a)
let a = 12

过程:

  1. ECStack
  2. EC(G)
  3. VO(G)
  4. 代码执行
    1. console.log(a) -> Uncaught ReferenceError: a is not defined -> 执行到这儿的时候,看到VO(G)里边咩有a
    2. let a = 12 -> 这时VO(G)旗下多了 a = 12

可见区别1:let声明的变量不存在变量提升

在现在的项目里边,创建函数,一般都是基于函数表达式来实现的,毕竟这可以防止其提前变量提升

如:

fn() // Uncaught ReferenceError: fn is not defined
let fn = function () {

}

一个奇葩的题:

fn();
function fn(){ console.log(1); }
fn();
function fn(){ console.log(2); }
fn();
var fn = function(){ console.log(3); }
fn();
function fn(){ console.log(4); }
fn();
function fn(){ console.log(5); }
fn();

分析:

  1. ECStack
  2. EC(G)
  3. AO(G)
    1. fn -> undefined -> fn() console.log(5) -> console.log(3)
  4. 代码执行
    1. 5
    2. 5
    3. 5
    4. fn = function(){ console.log(3); }
    5. 3
    6. 3
    7. 3

测试正确

老师解析:

/*
* EC(G)
*   VO(G)
*     fn = AAAFFF111
*        = AAAFFF222
*        = AAAFFF444
*        = AAAFFF555 (变量提升阶段完成,fn=AAAFFF555)
* 代码执行
*/

直接忽视掉 var fn的声明

2、区别二

如:

var x = 12
console.log(window.x) //12

let y = 12
console.log(window.y) //undefined

或许你会疑问,我们在函数里边声明的私有变量,是否也会让函数有个类似 fn.x这样的属性?

如:

function fn() {
  var x = 100
  console.log(fn.x) //undefined
  console.log(window.x) //undefined
}
fn()

可见,这是咩有的

所以说,这个给GO添加属性的特点,仅限于在全局用var声明创建的变量,而私有EC里边的私有变量,则没这种「嗜好」

还有一种特殊情况:

function fn() {
  x = 100
}

// fn里边的x=100,在这里相当于给全局写上 x = 100

分析这种情况:

当然,这种情况太片门了,我们一般都不会写这样的烂代码,所以理不理解它也没事儿……

既然这个设计是烂的,为何要去记住它呢?

题外话:

在Node.js 环境下:

repl下:

node环境

node xx.js

不会有全局属性

a这个变量是被收纳到 1.js这个模块对象旗下了……而不是全局对象旗下

所以,你要注意,同一个代码,你在浏览器执行,和在Node.js 环境下执行,也可能会不一样,尤其是涉及到 window这样的全局对象

3、区别三

var这个语法本身就是不严谨的,在编译阶段,浏览器知道它不严谨,所以在面对 var x = 12;var x = 13;的时候,就睁一只眼闭一只眼了。而let声明的变量则不行,一旦重复声明,代码就不会执行了,直接在变编译阶段给报错了!

可以重复声明情况:

var a = 10
var a = 20
console.log(a)

不允许重复声明情况:

console.log('hi')
let a = 10
console.log(a)
// 你以为执行到这一行才会报错吗?非也……
let a = 20
console.log(a)

我们知道浏览器拿到一份JS代码:

  1. 编译阶段(编译器)
    1. 词法解析 -> …… -> AST抽象语法树(给浏览器引擎去运行的)
  2. 引擎执行阶段
    1. ECStack -> EC(G) -> GO、VO(G)……

在编译阶段,重复用let声明的变量,就直接GG掉 -> 全部代码也不会被执行

总之:

带var的是可以重复声明的(词法解析审核通过),执行阶段遇到已经声明过的变量,则不会再重新声明

就像是第二次看到var a就说了句「咦,已经声明过了呀!既然如此,那就看下一行代码 或者说 类似点到一样,看到重复的学号就不用再点一次了,因为ta已经在了啊!如果再点一次,岂不是多一次一举?」 -> 其实说白了你TM重复声明就不应该出现,太TM不严谨了 -> 当然,如果你是为了容错率,那当我没说哈……

但是let是不可以这样的,这样的代码在词法解析阶段都过不去,所以也就不存在浏览器引擎去执行代码的阶段了

你可以 AST 一下,你会看到:


用let体现代码在执行阶段的报错(可否变量提升?):

console.log('hi')
console.log(x)
let x = 5

let体现执行阶段报错

从这可以侧面看出,变量提升是在代码执行阶段搞的,而不是在构建AST这个过程搞的!

回过头来看,用let体现编译阶段的报错(重复声明):

let编译阶段报错


4、区别四(很重要)

一道很常见的面试题:

// 10ms后连续输出五个5,10ms是个理论时间,实际上要比10ms要大
for (var i = 0; i < 5; i++) {
  // 定时器是异步操作:不用等定时器到时间,继续开启下一轮循环
  setTimeout(() => {
    console.log(i);
  }, 10);
}

等EC(G)清空后,才把 callback push 到 ECStack 里边去执行

然而,我们的需求并不是连续输出五个5,而是等循环结束后连续输出0~4

不用 let 的情况下,使用闭包来完成这个需求:

// 连续输出0、1、2、3、4
for (var i = 0; i < 5; i++) {
  !(function (i) {
    // window.setTimeout -> 全局占用了这个 callback heap
    setTimeout(() => {
      console.log(i);
    }, 10);
  })(i);
}

为啥会这样呢?

为啥自执行的EC不会被销毁?

  1. EC(自执行)
  2. AO(自执行)
    1. i = 0~4
    2. 创建一个匿名函数(callback) -> ()=>{}0x101~0x105) -> 0x101~0x105[[scope]]:AO(自执行)
  3. 自执行函数代码执行
    1. window.setTimeout(0x101~0x105,10) -> 全局API关联了(用到了)callback,而callback隶属于AO(自执行) -> 所以自执行EC不会被销毁

eventloop

EC(G)执行完毕后?

  1. 有5个callback排队着在等待入栈执行(哪个callback先到点,哪个就先排队,等EC(G)GG后就入栈执行)
  2. callback1入栈
    1. EC(0x101) -> AO(0x101) -> scopeChain<AO(0x101),AO(自执行1)> -> console.log(i) -> 找到AO(自执行1) -> 找到i0 -> log 0
  3. 同理,如第二步这样
  4. ……

我之前是不太理解上边这个代码的,而在这篇文章里边,说到:

编程界崇尚以简洁优雅为美,很多时候

如果你觉得一个概念很复杂,那么很可能是你理解错了。

具体点,在我看来的话,如果你觉得一个概念很复杂,那么很可能是你欠缺了很重要的前置知识……当你补上这些前置知识后,你还得需要一些很好例子,来引导你完成对这个「复杂」概念的理解…… -> 难者不会,会者不难

如:如果没有「ECStack、EC(G)、EC(fn)、GO、AO/VO、scope、scopeChain等这样的概念」+「老师在恰当的时机给出的这些例子」,那么我是真得无法在很大程度上理解闭包这个东西的,更不用说理解上边那个代码了


回过头来看,上边这种写法:

所以说,在真实项目里边要尽可能减少对闭包的使用

可是,除此之外,难道还有其它姿势可以满足此需求?

此姿势甚是简单,只需把 var 改成 let 即可:

// 其内部实现原理:跟上边那个闭包原理一模一样……没啥区别
for (let i = 0; i < 5; i++) {
   setTimeout(() => {
      console.log(i);
   }, 10);
}

为啥改成let就可以了呢?而且上边列出的三段代码,一同执行居然没有报错,我本以为用let声明的变量 i 会报错,谁知道居然咩有

因为「let存在块级作用域」呀!而var则是没有的!

具体点来说,就像这样:

{
   // -> 父级作用域
   let i = 0;

   // 第一轮循环
   {
      //  -> 子块作用域
      let i = 0;
      setTimeout( () => { console.log(i); }, 10);
   }
   i++; // -> i = 1
   // 第二轮循环
   {
      //  -> 子块作用域
      let i = 1;
      setTimeout( () => { console.log(i); }, 10);
   }
   // ……同理……
}

可以看到,这跟我们之前讲的闭包没啥区别:

  1. 父块作用域 -> 粗略看成是 -> 之前全局作用域
  2. 子块作用域 -> 相当于 -> 之前每次形成的闭包,即自执行函数的私有变量+callback(芳芳的终极理解)
  3. 私有变量i是由父块传进来的 -> 之前全局变量i是通过给自执行函数传参进来的

可以看到,这同闭包那套思想是一模一样的,只不过,这里是通过一个let就可以帮我们搞定之前那些麻烦事儿:

不过,这有一点不好,那就是无法访问在全局访问这个 i

无法访问i

用来累积的i是父块作用域的,不是全局的,即你无法在全局使用这个i

所以,let 和 var 的另一个区别是:

题外话,形成块级作用域的(不算对象那个{},因为只有key和value,无法声明一个变量,如果你真这样声明一个变量,那么会报语法错误的):

if(){}

let & if

switch(){case}

let & switch

即便,你给了break也会报错:

console.log("hi");
let a = 100;
switch (a) {
   case 100:
      let x = 200;
      break;
   case 200:
      let x = 300;
   default:
      break;
}

不过,你这样就不会报错了:

console.log("hi");
let a = 100;
switch (a) {
   case 100:
      {
         let x = 200;
      }
      break;
   case 200:
      let x = 300;
   default:
      break;
}

代码合理情况:

let & switch

try{}catch(e){}

try-catch

不完全归纳法:

除了对象的那个{},其它的只要出现了 {} 基本上你使用了let/const声明的变量就构成了块级作用域

5、区别五

具体体现:

暂时性死区

制定规则的人咩有考虑到这种情况

◇const vs let

let 与 var有啥区别,那const与var就有啥区别

let 与 const 的唯一区别:

let 创建的变量是可以更改指针指向的(也就是可以重新赋值的),而用const声明的变量是不允许改变指针指向的。

如:

let vs const

当面试官问用const声明一个常量,不用咬文嚼字;当面试官读Vue为「V-U-E」时,也不用咬文嚼字

★总结

★Q&A

1)变量的重复声明?

JS的容错率很高,一些其他语言常见的小错误JS都能大度得包容,比如给一个方法传入超出预计的参数、在声明变量之前使用该变量(变量的声明提升解决了这个问题)等等

遇到重复用var声明的变量就跳过之,而遇到函数则定义之

➹:JS变量重复声明以及忽略var 声明的问题及其背后的原理_javascript_SuperCoooooder的博客-CSDN博客

2)浏览器端的event loop?

「event loop」 -> 谷歌图片搜索 -> 看到哪个图片好就点进去,顺藤摸瓜找到图片所在的文章……

➹:JavaScript Concurrency Model and Event Loop

➹:What is the Event Loop in JavaScript? - JavaScriptBit

➹:JavaScript Event Loop

3)知识断层

知识断层

知识是具有延续性的,如果出现大的变故,如战争之类的,就有可能造成知识断层,出现知识不延续现象。

对于国家、社会、世界来说,发生战争,就有可能造成知识断层,出现知识不延续现象。

对于个人,从学校毕业之后,如果连续几年不看报纸、杂志、书籍,不听广播、不看电视中的新闻、综艺等节目,就可能出现个人知识断层,跟不上时代。

在山里隐居……

两个概念:

  1. 知识断层。是指在某一个较长的时间段内,孩子没认真学习,老师讲课,孩子基本不学习,故这段时间老师所讲知识点连续空档。这种断档知识点是连续的,也就是连续知识点整体塌陷。
  2. 知识断点。是指孩子在学习中,因为种种原因,偶尔对某几个间断知识点没听懂,这种知识断点是间隔性的、不连续的。

一个是连续的一片,一个是间隔的不连续的零碎小点

如何解决?

  1. 对断档的知识点产生的影响分析 -> 由于拉的间隔比较长很难补上,会在以后的学习中产生严重影响,表现在成绩持续低迷。知识断层就像一段隐藏的鸿沟,无论学生怎么努力,怎样进行学习方法的训练,但是成绩提升却非常缓慢,因为学生本人根本看不到这段鸿沟在哪?但是,只要找到了这段鸿沟在哪,就有办法填补
  2. 对断点知识点的产生的影响分析 -> 断点知识点就像一根链条上有几处分散断点,只要把这几处断点接上了,就会形成完整的知识链。很多时候,这种断点是在训练套卷中无意中补上的。这种知识断点在通过系统训练套卷中会很快弥补上。故仅仅存在知识断点的学生,训练有效的学习方法会提分很快。 -> 我的前端学习应该出现知识断点了,我要通过刷面试题来无意中补上吗?
  3. 对知识断层的对策。因为形成知识断层的原因多数由恋爱、网瘾、生病休假引起的,这是一个比较清楚且持续时间比较长的空档,只要计算准确这段时间,可以把这段时间中作业、试卷中的错题集中整理后,请一对一老师快速补上就ok了;或者在一轮复习中注意听课弥补这段知识鸿沟。
  4. 对知识断点的对策。多做套卷—查找漏洞点—比对答案—复习课本—问老师和同学—重做,经过着呢一个过程,知识断点会很快补上

➹:对于知识断层和断点的分析判断和对策

➹:「知乎知识库」— 断层 - 知乎

➹:知识断层是什么意思_百度知道

➹:是否会有某个时间人类智商出现断层,有某一代很笨,无法理解现代知识,导致当今人类科研被浪费? - 知乎

➹:中国人社问题─文化不自信导致的价值体系紊乱和知识断层的后果 - 知乎


4)难者不会,会者不难

做任何事情,都要有方法 -> 害怕难的人学不会,会做的人不感到难

做任何事情,都要有方法。如果你知道做某一件事情的最佳方法。那么,你会觉得很好做,一点也不难。困难的事情对于一般人来说,难以解决是因为不懂,而对于会的人或行家来说,一点不难

做任何事情,都要有方法。如果你知道做某一件事情的最佳方法。那么,你会觉得很好做,一点也不难。这就是「会者不难」。同样,如果你不知道做某一件事情的最佳方法,那么,你会觉得很难做。这就是「难者不会」。

举例:

  1. 比如,学习难吗?掌握了好的学习方法的人,学习将很轻松。而没有掌握好的学习方法的人,学习将很费劲。再比如:炒股难吗?会的人,轻轻松松地赢钱。不会的人,整天疲于奔命地亏钱。为什么?就在于你是否掌握了最佳方法。
  2. 比如,刚学算术的时候,加减乘除很难;刚学写字的时候,“大”字都很难,再回头看看,都成为小菜一碟。

出处:

清·王濬卿《冷眼观》第十二回:“这个就叫做难者不会,会者不难了。我如明明的来伙你去骗人,你又怎能知道我伙人来骗你呢?”

意义:

这就告诉我们要学会建立一种积极的心态,认识困难,承认困难,迎战困难,战胜困难,在解决困难的过程中成长并体验到乐趣与成就感,体会到什么叫做会者不难,这比单单的学习知识练习技能要关键的多,在不断的克服重重困难的过程中,我们的经验知识越来越多,而我们也会有更多的事情会“会者不难”

➹:难者不会,会者不难_百度百科

➹:难者不会,会者不难的意思_拼音是什么_成语解释_造句_近义词_反义词_汉辞网

5)一个let 与 for循环的疑惑点

let & i

证明确实有两个作用域:

for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域

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

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

可以搞个临时变量:

两个作用域

话说,for是这样,那 if(){}里边的()呢?

console.log('hi')
if(let a = 1) {
   let a = 5
   console.log(a)
}
console.log(a)

语法报错哈,构建AST过程中就GG了!

这就像是在对象里边声明一个变量一样!

➹:let 和 const 命令 - ECMAScript 6入门

6)临时性死区?

感觉有矛盾、理解不够深刻,可以死磕一下……

看完老师讲解后,没看这篇 我用了两个月的时间才理解 let - 知乎 文章的理解,正如这篇文章里边开篇所提到的这样:

说到JS变量,那么就不得不提它有关它的三件事了:

可是let到底有没有提升呢?

  1. let 的「创建」过程被提升了,但是初始化没有提升。 -> 如果一个变量咩有被初始化,那么是无法被使用的,它的报错信息有两种,在浏览器直接运行代码是「xx is not defined」;写在一个script标签里边,打开浏览器运行,则是报「在初始化xx之前是无法访问xx的」
  2. var 的「创建」和「初始化」都被提升了。 -> ES规范var声明的变量初始化为undefined
  3. function 的「创建」「初始化」和「赋值」都被提升了。 -> 如果var afunction a(){}同时出现(无关先后出现顺序),那么在代码执行之前,a的值就是函数值,因为函数是有个「赋值」过程,假如function在前,总不能赋值过后又初始化吧?假如var在前,初始化后,然后赋值一个函数值,不是很符合道理吗?至于你用 let a,在构建AST的过程中就报错了,即「 let 发现重名,叫你滚去改代码」 -> 简单来说「function 比 var 牛逼

const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。

3种声明的变量

所谓暂时死区,就是不能在初始化之前,使用变量。 -> 这一点是在EC完成的,即你EC入栈的时候,一开始要init一下这个EC的环境,如创建变量啥的,也叫声明变量哈……

题外话:

➹:javascript - Are variables declared with let or const not hoisted in ES6? - Stack Overflow

7)词法环境和执行上下文是一个意思吗?

差不多一个意思!

EC入栈,做两件事:


为啥说是差不多一个意思呢?而不是就是一个意思呢?

Lexical Environments(词法环境),之所以叫词法环境,是因为它是和源程序的结构对应,就是和你所写的那些源码的文字的结构对应,你写代码的时候这个环境就定了。Lexical Environments(词法环境)和四个类型的代码结构相对应:

为啥这就叫词法环境呢? -> 不知道,ES5就是这么规定设计的。。

词法环境

我们知道只有在全局代码、函数代码、和eval代码三种情况,才会创建运行上下文,而这上边居然会出现有5种?

EC

可以简单认为术语execution context视为当前代码被认定为的环境(environment)/作用域(scope),但其实作用域指的是AO/VO

所以这就是词法环境与EC的不同之处呀! -> 前者有5个,而后者有3个

为啥会有不同呢? -> 因为with结构,catch结构,不会创建AO/VO这样的东西

每次EC入栈,都会有这么两个过程:

EnvironmentRecord有两种:

➹:1. 彻底搞懂javascript-词法环境(Lexical Environments) - 掘金

➹:重学js —— Lexical Environments(词法环境)和 Environment Records(环境记录) · Issue #49 · lizhongzhen11/lizz-blog

➹:图解JS词法环境「Lexical environment」_javascript_public class Me的博客-CSDN博客