# var let const
ES5 只有全局作用域和函数作用域,没有块级作用域。这带来很多不合理的场景:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:
- 函数内部
- 块中(字符 { 和 } 之间的区域)
# 特点
块级声明用于声明在指定块的作用域之外无法访问的变量。let 和 const 都是块级声明的一种。特点:
不存在变量提升
var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined。为了纠正这种现象,let 或 const 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
暂时性死区现象(TDZ: Temporal Dead Zone)
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。 ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
在 let 和 const 声明变量之前,只要变量在还没有声明完成前使用,就会报错,都属于该变量的暂时性死区
let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错。这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
不允许重复声明
let 不允许在相同作用域内,重复声明同一个变量。重复声明报错。
不绑定全局作用域
当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性。然而 let 和 const 不会。
# 区别 重点
# let 与 var 的区别
- let 不存在变量提升
var 存在 变量提升 现象,即变量可以在声明之前使用,值为 undefined。let 或 const 所声明的变量一定要在声明后使用,否则报错。
- let 不允许重复声明
let 不允许在相同作用域内,重复声明同一个变量,重复声明报错。var 重复声明会覆盖或忽略。const 声明的常量,也与 let 一样不可重复声明。
- let 不绑定全局作用域
当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性。然而 let 和 const 不会。
- let 存在暂时性死区现象
在 let 和 const 声明变量之前,只要变量在还没有声明完成前使用,就会报错,都属于该变量的 暂时性死区。即,let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错。let 和 const 声明的变量会放在 TDZ 暂时性死区里面,访问暂时性死区的变量就会报错,且检测报错发生在词法解析(AST词法解析树)阶段,而不是在代码执行阶段。
- let 会产生块级作用域
let 或 const 只在声明所在的块级作用域内有效,即,块级作用域中有效。块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
# let 与 const 的区别
- const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const 声明的变量不得改变值,这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。对于 const 来说,只声明不赋值,就会报错。const 声明不允许修改绑定,但允许修改值。这意味着当用 const 声明对象时,可以修改对象的属性值,只是不能修改对象在栈中的引用地址。
- const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。
什么是块级作用域
大括号 {} 中存在 let,const,function都被认为是块级作用域。这个大括号的范围内相当于一个块级作用域,在执行时会创建私有上下文。注意的是函数也存在变量提升现象,但在块级作用域中的函数,在变量提升阶段,对此函数只声明不定义。块级作用域对 var 不生效。
let 在循环中
let 声明在循环内部的行为是标准中专门定义的,不一定就与 let 的不提升特性有关。例如,在 for 循环中使用 let 和 var,底层会使用不同的处理方式。简单的来说,就是在 for (let i = 0; i < 3; i++)
中,即圆括号之内建立一个隐藏的作用域,然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。
for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var 命令和 function 命令。
ES6 除了添加 let 和 const 命令,另外两种声明变量的方法:import 命令和 class 命令。所以,ES6 一共有 6 种声明变量的方法分别是: var,let,const,function,class,import。
let const 使用场景:
- let 使用场景:变量,用以替代 var。
- const 使用场景:常量、声明匿名函数、箭头函数的时候。
# 总结
ES6 规定暂时性死区和 let、const 语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。
- 允许在块级作用域内声明函数。
- 函数声明类似于 var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于 var 声明的变量。考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。严格模式下,函数只能声明在当前作用域的顶层。
const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
# 问题
# Q1. 为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:
- 内层变量可能会覆盖外层变量
- 用来计数的循环变量泄露为全局变量
let 实际上为 JavaScript 新增了块级作用域。块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
# Q2. 函数能不能在块级作用域之中声明?
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于 var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作 let 处理。根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于 var 声明的变量。考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
← 参数传递与 Arguments 箭头函数 →