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

var temp = new Date();
function go () {
  console.log(temp);
  if(false){
    var temp = 'hello world';
  }
}
go(); // undefined

上面代码的本意是,if代码块的外部使用外层的temp变量,内部使用内层的temp变量。但是go函数执行后,输出的结果是undefined,原因在于变量提升导致内层的temp变量覆盖了外层的temp变量。

还有一种情况,用来计数的循环变量泄露为全局变量。如:

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

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

ES6块级作用域
let实际上为Javascript新增了块级作用域。

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

上面代码的函数有两个代码块都声明了变量n,运行后结果输出5。这就表明外层代码块不受内层代码块的影响。如果使用var定义变量的n话,最后输出的值就是10了。
在ES6中允许块级作用域的任意嵌套。如:

{{{{ let text = 'Hello World'; }}}};

上面代码嵌套了4层的块级作用域。外层作用域无法读取内层作用域的变量。如:

{{{{
  {let text = 'Hello World';}
  console.log(text);// 报错
}}}};

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

{{{{
  let text = 'Hello World';
  {let text = 'Hello World';}
}}}};

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

// IIFE写法
(function (){
  var temp = 'hello';
  ...
}());
// 块级作用域写法
{
  let temp = 'hello';
  ...
}

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

// 情况一
if(true){
  function f() {}
}
// 情况二
try{
  function f() {}
}catch(e){
  //...
}

上面这两种函数声明在ES5中都是非法的。但是实际情况是,以上两种情况都能运行,并不会报错。原因是,浏览器没有遵守这个规定,浏览器为了兼容以前的旧代码,还是支持在块级作用域中声明函数。
在ES6中引入了块级作用域,明确允许在块级作用域中声明函数。ES6规定,在块级作用域中函数声明语句的行为类似于let,在块级作用域之外不可应用。如:

function f() {console.log('我在外面');}
(function(){
  if(false){
    function f() {console.log('我在里面');}
  }
  f();
}());

上面代码在ES5中的运行结果是 “我在里面”,因为在if内声明的函数f会被提升到函数头部,实际代码运行如下:

// ES5环境
function f() {console.log('我在外面');}
(function(){
  function f() {console.log('我在里面');}
  if(false){  
  }
  f();
}());

而在ES6中运行就完全不一样了,理论上会得到“我在外面”。因为在块级作用域内声明的函数类似于let,对作用域之外的没有影响。
但是真正在ES6浏览器中运行却会报错。这是不是很奇怪?
由于如果改变了块级作用域内声明的函数的处理规则,显然势必会对旧的代码产生非常大的影响。为了减轻因此产生的不兼容问题,ES6在附录B(http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics)中规定,浏览器的实现可以不遵守上面的规定,可以有自己的行为方式(https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6),具体如下:

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

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

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

需要特别注意,上面3条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

根据这3条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。如:

// 浏览器的 ES6 环境
function f() { console.log('我在外面'); }
(function () {
  if (false) {
    function f() { console.log('我在里面'); }
  }
  f();
}());
// Uncaught TypeError: f is not a function

尽管如此,上面的代码在符合ES6的浏览器中都会报错,因为实际运行的下面的代码:

// 浏览器的 ES6 环境
function f() { console.log('我在外面'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('我在里面'); }
  }
  f();
}());
// Uncaught TypeError: f is not a function

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

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

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

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

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

{
  let t = f();
  t = t * t + 1;
}

在ES6中有一个提案(http://wiki.ecmascript.org/doku.php?id=strawman:do_expressions),使得块级作用域可以变为表达式,即可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式。如:

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

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

 

标签: JavaScript, ES6, 作用域, JS作用域, 块级作用域

添加新评论