JS学习梳理(1)JS块级作用域

604 阅读7分钟

前言

在之前的文章中,有聊到过js变量提升的相关内容,正式由于 JavaScript 存在变量提升这种特性,从而导致了很多与我们直觉不符的代码,也容易出现bug,这也是 JavaScript 的一个重要设计缺陷。所以下面的文章会介绍如何通过块级作用域并配合 let 和 const关键字来修复这种缺陷。

作用域(scope)

为什么 JavaScript 中会存在变量提升这个特性,而其他语言似乎都没有这个特性呢? 要讲清楚这个问题,我们就得先从作用域讲起。

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

在 ES6 之前,JavaScript 只支持这两种作用域,相较而言,其他语言则都普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

为了更好地理解块级作用域,你可以参考下面的一些示例代码:


//if块
if(1){}

//while块
while(1){}

//函数块
function foo(){}
 
//for循环块
for(let i = 0; i<100; i++){}

//单独一个块
{}

简单来讲,如果一种语言支持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。

ES6 之前是不支持块级作用域的,没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。

变量提升所带来的问题

1. 变量容易在不被察觉的情况下被覆盖掉

示例代码如下:


var myname = "念念不忘"
function showName(){
  console.log(myname);
  if(0){
   var myname = "静静"
  }
  console.log(myname);
}
showName()

执行上面这段代码,打印出来的是 undefined,而并没有像我们希望的那样打印出来“念念不忘”的字符串。为什么输出的内容是 undefined 呢?我们再来分析一下

showName 函数的执行上下文创建后,JavaScript 引擎便开始执行 showName 函数内部的代码了。首先执行的是:

console.log(myname);

执行这段代码需要使用变量 myname,我们看到这里有两个 myname 变量:一个在全局执行上下文中,其值是“念念不忘”;另外一个在 showName 函数的执行上下文中,其值是 undefined。JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量 myname,而值是 undefined,所以获取到的 myname 的值就是 undefined,所以获取到的 myname 的值就是 undefined。这输出的结果和其他大部分支持块级作用域的语言都不一样,所以这会很容易造成误解。

2. 本应销毁的变量没有被销毁接下来我们再来看下面这段让人误解更大的代码:

示例代码如下:


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

如果你使用 C 语言或者其他的大部分语言实现类似代码,在 for 循环结束之后,i 就已经被销毁了,但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 7。这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。这依旧和其他支持块级作用域的语言表现是不一致的,所以必然会给一些人造成误解。

ES6 是如何解决变量提升带来的缺陷

上面我们介绍了变量提升而带来的一系列问题,为了解决这些问题,ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

关于 let 和 const 的用法,你可以参考下面代码:

let x = 5
const y = 6
x = 7
y = 9 //报错,const声明的变量不可以修改

从这段代码你可以看出来,两者之间的区别是,使用 let 关键字声明的变量是可以被改变的,而使用 const 声明的变量其值是不可以被改变的。但不管怎样,两者都可以生成块级作用域,为了简单起见,在下面的代码中,我统一使用 let 关键字来演示。那么接下来,我们就通过实际的例子来分析下,ES6 是如何通过块级作用域来解决上面的问题的。

可以先参考下面这段存在变量提升的代码:

function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // 同样的变量!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

在这段代码中,有两个地方都定义了变量 x,第一个地方在函数块的顶部,第二个地方在 if 块的内部,由于 var 的作用范围是整个函数, 最终只生成了一个变量 x,函数体内所有对 x 的赋值操作都会直接改变变量环境中的 x 值。

所以上述代码最后通过 console.log(x) 输出的是 2,而对于相同逻辑的代码,其他语言最后一步输出的值应该是 1,因为在 if 块里面的声明不应该影响到块外面的变量。

既然支持块级作用域和不支持块级作用域的代码执行逻辑是不一样的,那么接下来我们就来改造上面的代码,让其支持块级作用域。

这个改造过程其实很简单,只需要把 var 关键字替换为 let 关键字,改造后的代码如下:


function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

执行这段代码,其输出结果就和我们的预期是一致的。这是因为 let 关键字是支持块级作用域的,所以在编译阶段,JavaScript 引擎并不会把 if 块中通过 let 声明的变量存放到变量环境中,这也就意味着在 if 块通过 let 声明的关键字,并不会提升到全函数可见。所以在 if 块之内打印出来的值是 2,跳出语块之后,打印出来的值就是 1 了。这种就非常符合我们的编程习惯了:作用域块内声明的变量不影响块外面的变量。

总结

由于 JavaScript 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。所以梳理下var、let、const的区别:

  • var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
  • let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
  • const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。同一个变量只能使用一种方式声明,不然会报错