JavaScript执行机制 - 块级作用域

745 阅读3分钟

背景

在变量提升的文章中,我们知道了在执行上下文被创建时,用varfunction定义的变量和函数的声明会被提前,并以默认值为变量赋值。

由于变量提升的存在,其也会带来某些问题。

让我们分析如下代码:

var text = "Hello world";
function func(){ 
  console.log(text); 
  if(0){ 
    var text = "???" 
  } 
  console.log(text);
}
func()

两个log都打印了undefined,第一个log打印undefined是因为变量提升,第二个log打印undefined是因为赋值语句没有被运行而text的值依然是undefined。

由于变量提升这种特性的存在,变量可能在不经意间被替换掉了。

让我们再看一种场景:

function foo(){
  for (var i = 0; i < 10; i++) {
  }
  console.log(i); 
}
foo()

i用于控制for循环的迭代,本应在循环结束而销毁的变量依然可以在下面的代码中被访问。

正是由于JavaScript存在这种变量提升的特性,从而导致很多让人迷惑的代码,这也是JavaScript的重要设计缺陷。

为了避开这种设计缺陷在ECMAScript 6标准中,引入了关键字letconst来实现块级作用域

作用域

作用域指程序中声明变量的空间,同时也维护了的变量的生命周期。

在块级作用域出现之前,JavaScript只支持两种作用域:

  • 全局作用域:在全局作用域中定义的变量可以在任何位置访问,其随着页面关闭而销毁。
  • 函数作用域:在函数内定义的变量与函数,只能在函数内被访问,其随着函数的运行结束而销毁。

而新加入的块级作用域可以将变量定义在一对大括号中。例如函数、判断、循环等,甚至在单独的大括号中依然有效。

//if块
if(1){}
//while块
while(1){}
//函数块
function foo(){}
 
//for循环块
for(let i = 0; i<100; i++){}
//单独一个块
{}

让我们对开篇时的代码稍作修改:

let text = "Hello world";
function func(){ 
  console.log(text); 
  if(1){ 
    let text = "???" 
    console.log(text);
  } 
  console.log(text);
}
func()

运行下就能看出明显的不同。

JavaScript块级作用域的实现

这里我们需要执行上下文,我们知道用varfunction关键字定义的变量和函数会存放在执行上下文中的变量环境中。其实在执行上下文中还有一个叫做词法环境的空间,用来存放用letconst定义的变量。先让我们观察如下代码。

function foo(){
  var a = 1
  let b = 2
  {
    let b = 3
    var c = 4
    let d = 5
    console.log(a) // 1
    console.log(b)// 3
  }
  console.log(b) // 2
  console.log(c) // 4
  console.log(d) // Uncaught ReferenceError: d is not defined
}   
foo()

其中a、c用var定义,会存储在变量环境中,b、d用let定义存储在词法环境中。

因为块作用域以大括号做为作用范围,则第4行的变量b和第6行的变量6存在于两个块作用域里。而第10行和第12行访问的也是不同块作用域的变量b,这也印证了为什么第14行访问变量d时为什么会报错。

关于作用域中变量查找的方式,会在作用域链的章节中说到。

总结

通过引入块作用域,让我们可以使用let和const关键字避开变量提升的缺陷。关于let和const的特点会在ECMAScript新特性的章节中做更深入的介绍。