二、新的ECMA代码执行描述和let/const/var及块级作用域的学习

2022/8/3 ES6语法环境环境记录let关键字const关键字let/const与var的区别块级作用域

# 1、ES5及以上出现的新的ECMA代码执行描述概念

# 1.1.新的ECMA代码执行描述

  • 在执行学习JavaScript代码执行过程中,我们学习了很多ECMA文档的术语:

    • 执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
    • 执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
    • 变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
    • 全局对象:Global Object,全局执行上下文关联的VO对象;
    • 激活(活动)对象:Activation Object,函数执行上下文关联的VO对象;
    • 作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
  • 在新的ECMA代码执行描述中(ES5以及之上),对于代码的执行流程描述改成了另外的一些词汇:

    • 基本思路是相同的,只是对于一些词汇的描述发生了改变;
    • 执行上下文栈和执行上下文也是相同的;

# 1.2.词法环境(LexicalEnvironments)

  • 词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;
    • 一个词法环境是由环境记录(Environment Record)和一个外部词法环境(oute;r Lexical Environment)组成;
    • 一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来;
    • ECMA官方描述图片翻译:词法环境(Lexical Environment)是一种规范类型,用于基于 ECMAScript 代码的词法嵌套结构,定义标识符与特定变量和函数的关联。词法环境(Lexical Environment)由一个环境记录(Environment Record)和一个可能对外部词法环境(Lexical Environment)的空引用(null reference)组成。通常,词法环境与 ECMAScript 代码的某些特定语法结构(如函数声明,块语句)相关联。每次计算这些代码时,都会创建一个新的词法环境。

vZkhwQ.png

  • 也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;
    • 那么执行上下文会关联哪些词法环境呢?
    • 如下表:ECMAScript 代码执行上下文的其他状态组件(下面图片翻译后的表)
组成部分 目的
词法环境 标识用于解析此执行上下文中的代码所做的标识符引用的词法环境。
可变环境 标识其环境记录保存在此执行上下文中由变量语句创建的绑定的词法环境。

vZkIFs.png

# 1.3.LexicalEnvironment和VariableEnvironment

  • LexicalEnvironment用于存放let、const声明的标识符:
    • ECMA官方描述图片翻译:let 和 const 声明定义了作用域为运行执行上下文的词法环境的变量。**这些变量是在它们包含的词法环境被实例化的时候创建的,但是在变量的词法绑定被评估(求值)之前,不能以任何方式访问这些变量。**一个具有初始化器的词法绑定定义的变量在词法绑定被评估的时候,而不是在变量被创建的时候,赋值给它的初始化器的分配表达式。如果 let 声明中的词法绑定没有初始化器,则在计算词法绑定时,将为变量分配的值为undefined。

vZkHS0.png

  • VariableEnvironment用于存放var和function声明的标识符:
    • ECMA官方描述图片翻译:Var 语句声明了作用域为运行执行上下文的变量环境的变量。**Var 变量是在其包含的词法环境被实例化时创建的,并在创建时初始化为undefined。**在任何变量环境的范围内,通用的绑定标识符可以出现在多个变量声明中,但是这些声明仅共同定义一个变量。使用初始化器的变量声明定义的变量在执行变量声明时 而不是在创建变量时 被赋予其初始化器的赋值表达式的值

vZkjw4.png

# 1.4.环境记录(EnvironmentRecord)

  • 在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录。
    • 声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与ECMAScript语言值关联起来的Catch子句。
    • 对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性关联起来。

vZApf1.png

# 2、let/const的基本使用及和var的区别

# 2.1.let/const基本使用

  • 在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const

    • let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字;
    • 但是let、const确确实实给JavaScript带来一些不一样的东西;
  • let关键字:

    • 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量
  • const关键字:

    • const关键字是constant的单词的缩写,表示常量、衡量的意思;
    • 它表示保存的数据一旦被赋值,就不能被修改;
    • 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;
  • 注意:另外let、const不允许重复声明变量;

# 2.2.let/const没有作用域提升

  • let、const和var的另一个重要区别是作用域提升:
    • 我们知道var声明的变量是会进行作用域提升的;
    • 但是如果我们使用let/const声明的变量,在声明之前访问会报错;
console.log(age)
let age = 39
1
2
  • 那么是不是意味着age变量只有在代码执行阶段才会创建的呢?
    • 事实上并不是这样的,我们可以看一下ECMA262对let和const的描述;
    • 这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;

vZTCPs.png

  • 那let/const有没有作用域提升呢?
    • 从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的。
      • 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
    • 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
      • 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
      • 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;
    • 所以我的观点是let、const没有进行作用域提升,但是会在解析阶段被创建出来。

# 2.3.Window对象添加属性

  • 我们知道,在全局通过var来声明一个变量,事实上会在window上添加一个属性:

    • 但是let、const是不会给window上添加任何属性的。
  • 那么我们可能会想这个变量是保存在哪里呢?

  • 我们先回顾一下最新的ECMA标准中对执行上下文的描述

vZT0zt.png

  • 变量被保存到VariableMap中
    • 也就是说我们声明的变量和环境记录是被添加到变量环境中的:
      • 但是标准有没有规定这个对象是window对象或者其他对象呢?
      • 其实并没有,那么JS引擎在解析的时候,其实会有自己的实现;
      • 比如v8中其实是通过VariableMap的一个hashmap来实现它们的存储的。
      • 那么window对象呢?而window对象是早期的GO对象,在最新的实现中其实是浏览器添加的全局对象,并且一直保持了window和var之间值的相等性;

vZTfWn.png

# 2.4.let/const块级作用域

  • var的块级作用域
    • 在我们前面的学习中,JavaScript只会形成两个作用域:全局作用域和函数作用域。
    • ES5中放到一个代码中定义的变量,外面是可以访问的:
//var 没有块级作用域
{
  var age = 32
}
console.log(age)//32
1
2
3
4
5
  • let/const的块级作用域

    • n在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的:
    {
      let foo = 'foo'
      function bar() {
        console.log('bar')
      }
      class Person {}
    }
    bar()//bar
    console.log(foo)//报错
    var p = new Person()//报错
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    • 但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的:
      • 这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升;
  • 块级作用域的应用

<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<script>
  var btnEls = document.querySelectorAll('button')
for(let i=0;i<btnEls.length;i++) {
  btnEls[i].onclick = function() {
    console.log(`${i+1}个按钮`)
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# 2.5.var、let、const的选择

  • 那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?

  • 对于var的使用:

    • 我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗留问题;
    • 其实是JavaScript在设计之初的一种语言缺陷;
    • 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
    • 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;
  • 对于let、const:

    • 对于let和const来说,是目前开发中推荐使用的;
    • 我们会有限推荐使用const,这样可以保证数据的安全性不会被随意的篡改;
    • 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
    • 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;

# 3、let/const和var的区别总结

# 3.1.作用域提升:var(有),let/const(没有)

//1、var 存在作用域提升,
console.log(foo)//undefined
var foo = 'foo'
               
//2、let/const 不存在作用域提升
console.log(bar)//报错  这里会形成暂时性死区
let bar = 'bar'//const bar = 'bar'
1
2
3
4
5
6
7

# 3.2.同一作用域可以重复声明变量嘛?:var(可以),let/const(不可以)

//1、var 同一作用域可以重复声明变量
var foo = 'foo'
var foo = 'bar'

//2、let/const 同一作用域不可以重复声明变量
let bar = 'lyk'
let bar = 'code'//报错

//3.let/const 不同作用域可以重复声明变量
const baz = 'baz'
{const baz = 'lyk'}
1
2
3
4
5
6
7
8
9
10
11

# 3.3.在{},for(){},if(){},switch(){}中声明变量,这里的块级作用域是有效的吗?(ES6的代码块级作用域,什么时候才生效)

  • var声明变量:无效,不存在块级作用域
  • let/const声明变量:有效,存在块级作用域

​ 注意:for循环里面不能用const声明 i ,即for(const i = 0,i<arr.length,i++){},因为这里的 i 会进行自增i++,然后将自增的值赋给下一个作用域的i,而const只能声明常量,是不允许被修改的(这里的自增修改了i值,具体看如下:例子)

  • for...of遍历数组
var arr = ['cba','nba','abc']
//1、for循环不能用const来声明 i
for(let i,i<arr.length,i++){
  console.log(arr[i])
}

{
  let i = 0
  i++//自增,然后赋值给下面的i ,所以for循环不能用const来声明 i
}
{
  let i = 1
  i++
}
{
  let i = 2
}

//2、for...of 
for(const item of arr){
  console.log(item)
}
{const item = 'cba'}//这里每个作用域都有自己声明的item,互不影响,所以for...of可以用const声明 item
{const item = 'nba'}
{const item = 'abc'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • ES6的代码块级作用域(补充:ES6的代码块级作用域,对let/const/function/class声明的类型是有效)
// ES6的代码块级作用域
// 对let/const/function/class声明的类型是有效
{
  var age = 99
  let foo = "why"
  function demo() {
    console.log("demo function")
  }
  class Person {}
}
console.log(age)//99
// console.log(foo) // foo is not defined(报错)
demo()//没有报错,可以调用。 解释: 不同的浏览器有不同实现的(大部分浏览器为了兼容以前的代码, 让function是没有块级作用域)
var p = new Person() // Person is not defined(报错)


if(true){
  class People {}
}
var people = new People()// People is not defined(报错)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.4.暂时性死区

var foo = 'foo'
function bar() {
  console.log(foo)//报错,在此作用域中用  let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。(函数的词法环境被实例化时,这里foo已经创建出来了(所以不会去访问外层作用域的foo),只是不能访问,访问则报错)
  let foo = "abc"
}
bar()
//通过上面这个案例,我们其实也可以说let/const定义的变量有作用域提升,因为这里的foo没有访问外层作用域的foo,而是直接抛出错误
1
2
3
4
5
6
7

当程序的控制流程在新的作用域(module function 或 block作用域)进行实例化时,在此作用域中用 let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。

​ 因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。

​ 造成该错误的主要原因是:ES6新增的let、const关键字声明的变量会产生块级作用域,如果变量在当前作用域中被创建之前被创建出来,由于此时还未完成语法绑定,如果我们访问或使用该变量,就会产生暂时性死区的问题,由此我们可以得知,从变量的创建到语法绑定之间这一段空间,我们就可以理解为‘暂时性死区’

# 3.5.let和const的区别

  • let可以声明一个变量,可以被修改,重新赋值等...
  • const只能声明常量(衡量)它表示保存的数据一旦被赋值,就不能被修改;

​ 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;

const foo = 'foo'
foo = 'const'// 报错,这个是常量所以不能修改

const obj = {name:'lyk',age:18}
obj.age = 24
console.log(obj)//{name:'lyk',age:24}
obj = {name:'kobe',age:39}//报错,改变了引用类型(obj)的内存地址

const arr = [1,2,3,4,5]
arr[1] = 0
console.log(arr)//[1, 0, 3, 4, 5]
arr = []//报错,改变了引用类型(arr)的内存地址
1
2
3
4
5
6
7
8
9
10
11
12
  • 良好的代码编码习惯提醒:我们日常开发中最好声明的每一个变量都用const,后面如果发现该变量要修改,我们再把该变量改成用let声明

# 3.6.var和let/const 跟window的关系(具体看2.3)

  • var声明的变量会加到GO当中,即是浏览器的顶层全局对象:window对象中。
  • 而let/const声明的变量不会加到GO当中:用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中。那要怎么获取呢?在定义变量的块级作用域中就能获取,既然不属于顶层对象Window,那就不加 window(global),直接访问即可。
var foo = 'var'
console.log(window.foo)//var

let bar = 'let'
const baz = 'const'
console.log(window.bar,window.baz)//undefined,undefined
console.log(bar,baz)//let,const
1
2
3
4
5
6
7

具体可看该文章:文章 (opens new window)

# 3.7.总结: let、const 以及 var 的区别是什么?

  • let 和 const 定义的变量不会出现作用域提升,而 var 定义的变量会提升。
  • let 和 const 定义在语句块中,会形成块级作用域
  • let 和 const 不允许重复声明(会抛出错误)
  • let 和 const 定义的变量 在定义语句之前,如果使用变量 会抛出错误(形成了暂时性死区),而 var 不会。
  • const 声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
  • let/const定义的这些变量是在它们包含的词法环境被实例化的时候创建的,但是在变量的词法绑定被评估(求值)之前,不能以任何方式访问这些变量;(我们称包含这些变量的词法环境被实例化时 到 let/const定义的变量被词法绑定求值 的这一段时间叫暂时性死区)
最后更新时间: 2022/08/03, 22:51:39
彩虹
周杰伦