ES6 块级作用域

80 阅读2分钟

1. 为什么需要块级作用域?

ES5 只有全局作用域和函数作用域,没有块级作用域,这导致很多场景不合理。

1.1 内层变量可能会覆盖外层变量

var tmp = new Date()

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

f()
// undefined

以上代码的原意是,if 代码块的外部使用外层的 tmp 变量,内部使用内层的 tmp 变量。

但是由于变量提升,导致 if 代码块内层的 tmp 变量覆盖了外层的 tmp 变量。

所以需要块级作用域,将 if 代码块改为 let 声明即可。

函数内部可以直接读取全局变量。

1.2 用来计数的循环变量泄露为全局变量

var s = 'hello'

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

console.log(i)  // 5

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

2. ES6 的块级作用域

2.1 let 实际上为 JavaScript 新增了块级作用域。

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

输出 5,外层代码块不受内层代码块的影响。

如果使用了 var 定义变量 n,最后输出的值会是 10。

2.2 ES6 允许作用域的任意嵌套。

外层作用域无法读取内层作用域的变量

{{{{
  {let insane = 'hello world'}
  console.log(insance)  // 报错
}}}}

内层作用域可以定义外层作用域的同名变量。

{{{{
  let insane = 'hello world'
  {let insane = 'hello world'}
}}}}

2.3 IIFE

块级作用域的出现,实际上使得获得广泛应用的 立即执行匿名函数(IIFE)不再必要了。

// IIFE 写法
(function () {
  var tmp = …
  …
}());

// 块级作用域写法
{
  let tmp = …
  …
}

3. 块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

// ES5 非法情况1
if (true) {
  function f () {}
}

// ES5 非法情况2
try {
  function f () {}
} catch(e) {
  // …
}

上面两种函数声明在 ES5 中都是非法的。但是,浏览器为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际上都能运行,并不会报错。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,在块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。

function f() {  console.log('I am outside') }

(function () {
  if (false) {
    // 重复声明一次函数 f
    function f() {  console.log('I am inside') }
  }

  f()
} ());

以上代码在 ES5 中运行会得到 “I am inside”,因为在 if 内声明的函数 f 会被提升到函数头部。

而在 ES6 中运行就完全不一样了,理论上会得到“I am outside”。因为块级作用域内声明的函数类似于 let,对作用域之外没有影响。

但是,如果真的在 ES6 浏览器中运行上面的代码,是会报错的。

因为,如果改变了块级作用域内声明的函数的处理规则,显然会对就代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录B中规定,浏览器的实现可以不遵守上面的规定,而有自己的行为方式,具体如下:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于 var ,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

上面三条规则只对 ES6 的浏览器有效 其他环境不用遵守,仍旧将块级作用域的函数声明当作 let 处理即可。

所以,上面的代码在符合 ES6 的浏览器会报错,实际上运行的是以下代码:

// 浏览器的 ES6 环境
function f() {  console.log('I am outside') }

(function () {
  var f = undefined
  if (false) {
    function f() {  console.log('I am inside') }
  }

  f()
} ());
// Uncaught TypeError: f is not a function

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成 ** 函数表达式** 的形式,而不是函数声明语句。

// 函数声明语句
{
  let a = 'secret'
  function f () {
    return a
  }
}

// 函数表达式
{
  let a = 'secret'
  let f = function f () {
    return a
  }
}

另外,需要注意,ES6 的块级作用域允许声明函数的规则只在使用大括号的情况下成立,如果没有大括号,就会报错。

// 不报错
'use strict'
if (true) {
  function f () {}
}

// 报错
'use strict'
if (true)
  function f () {}

4. do 表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

现在有一个提案,使得块级作用域可以变为表达式,即可以返回值,办法是在块级作用域之前加上 do,使它变为 do 表达式。

let x = do {
  let t = f()
  t * t + 1
}

上面代码中,变量 x 会得到整个块级作用域的返回值。