块级作用域内函数和变量声明


为了方便后续引用理论依据进行说明,这里先给一些依据进行编号,依据来自 阮一峰的 ECMAScript 6 入门

  1. 允许在块级作用域内声明函数

  2. 函数声明类似于var,即会提升到全局作用域或函数作用域的头部

  3. 同时,函数声明还会提升到所在的块级作用域的头部

  4. var命令和function命令声明的全局变量,依旧是顶层对象的属性

  5. let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

从 4、5 两点看出,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。在以前,顶层对象的属性赋值与全局变量的赋值,是同一件事

本文代码测试均在谷歌浏览器(版本: 80.0.3987.149)测试,支持es6语法,其余浏览器执行结果可能会有所不同,不代表所有浏览器的结果。防止代码相互影响,测试均是在新的空tab下进行。

# 块级作用域内的变量声明

# 测试代码和结果

块级作用域内的变量声明

块级作用域内的变量声明

一些猜测

  1. 在 es6 中,直接给之前没有声明的变量赋值,会沿着作用域链一直查找,一直到顶级对象,然后进行赋值

  2. 在顶级对象上都没找到属性时,会报错。

# 猜测实际运行代码以及结果

块级作用域内的变量声明

# 如果存在 var 声明,结果就和之前的不太一样了

块级作用域内的变量声明

# 猜测实际执行代码和测试结果

块级作用域内的变量声明

# 解析

  1. 因为var声明会被提前,所以相当于在代码顶部添加了一段声明

  2. 根据依据4,var声明的全局变量,其实就是顶层对象的属性。所以这里不管是打印 a 还是 window.a ,其实最后通过作用域链找到的都是 window.a

# 另外一个验证测试

如果整个过程发生在函数内呢?

块级作用域内的变量声明

块级作用域内的变量声明

这个结果还是比较符合预期的

  1. 通过 var 声明时,声明被提升到函数顶部,然后真实赋值时,通过作用域链找到函数内的变量 a,所以后续打印 a 为 1,而 window.a 为 undefined

  2. 没有 var 命令时,赋值操作通过作用域链找到的是全局作用域,所以赋值在了全局,然后不管打印 a 还是 window.a 最后找到的都是 window.a

# 块级作用域内的函数声明

# 测试代码和结果

块级作用域内的变量声明

# 猜测实际执行代码及结果

块级作用域内的变量声明

# 解析

  1. 为什么代码顶部多了一段声明
  • 答:因为根据依据2,function 声明会提升到全局作用域或函数作用域的头部
  1. 为什么块级作用域顶部多了一段函数的声明和赋值
  • 答:根据依据3,函数式声明是会提升到所在块级顶部,
  1. 为什么提升到块级顶部的声明是let,而代码顶部是var
  • 答:因为实际上这个变量在块内是不能再次重复声明的,看图

块级作用域内的变量声明

这个时候代码块内的 var 声明居然报错了,而我们都知道,不管是var还是function的声明,不管重复几次,都是不会报错的!

  1. 为什么函数声明提升后,在原来的位置多了 window.a = a ?
  • 答:理由来自这段代码执行结果

块级作用域内的变量声明

先重点看 function 声明位置执行前后的打印,window 上的变量 a 居然被改了,而块作用域内的 a 不变。就像执行了 window.a = a 一样。 联系依据4,函数式声明,应该要声明到顶层对象上的,但是从第一个console.log我们发现,其实这个时候 window 上的 a 还只是 undefined,在真正执行了 function 声明这一行之后,window 上的 a 才被修改。

所以这里我觉得,函数声明在 es6 下的真正执行结果是这样

  1. 声明被提升到顶级作用域和块级作用域上

  2. 顶级作用域上只有声明,值为 undefined。而块作用域上不仅有声明,而且赋值为函数

  3. 等到执行到 function 声明所在位置时,顶级作用域上的变量才被赋值,但是注意,赋值的是当时的函数变量名,显然,这个函数变量名可以被重新赋值修改

# 块内有同名的函数和变量声明

# 一段测试代码

块级作用域内的变量声明

# 两个声明换个位置,测试结果并不一样

块级作用域内的变量声明

这段就不解释了,相信结合上面的说明应该能理解,就当作测试题吧!

# 总结

声明、赋值、取值,其实三个过程应该分开看了,特别是在 es6 之后,每个步骤都有它自己的一些特性。

  1. let、const 不能重复声明;而 var、function 可以

  2. var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;而 let 命令、const命令、class命令声明的全局变量,不属于顶层对象的属性

  3. 取值操作是会有整个作用域链查找的过程。如果在顶级对象上都没找到,会报错;但要注意的是,如果是通过 "." 操作符来取值,则不会报错,没有值会返回 undefined

  4. 赋值操作也会有整个作用域链的查找过程,和取值不一样的是,如果在顶级对象上都没找到,则会直接在顶级对象上添加这个属性

  5. var 和 function 在 es6 下依旧是有声明提升的,和原来一样,也是提升到最近的函数作用域或者全局作用域,且都只是声明,没有赋值。

  6. function 声明提升特别之处在于,它还会提升到块级作用域顶部,而且这时候不仅声明,还会赋值。而在原来的位置,留下一段类似 window.name = attrs 这样的代码