在前面 JavaScript 在浏览器中的执行机制-变量提升我们介绍了变量提升,了解了代码并不是完全按照顺序去执行。所以很多时候代码的运行顺序和我们的直觉并不一样,所以这是 JavaScript 的一个大坑 。
为了 填坑 在 ES6 引入了块级作用域的概念以及 let
和 const
两个关键字。但是 JS 是向下兼容的,很长一段时间内这个问题还是存在的,既要理解变量提升又要理解块级作用域。首先了解什么是作用域。
作用域
作用域 是当前的执行上下文,值和表达式在其中"可见"或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
在 ES6 之前 JavaScript 的作用域分以下三种:
- 全局作用域:脚本模式运行所有代码的默认作用域
- 模块作用域:模块模式中运行代码的作用域
- 函数作用域:由函数创建的作用域
在 ES6 之前 JavaScript 并不支持其他语言普遍支持的块级作用域 ,块级作用域就是使用一对{}
包裹的代码。比如函数、判断语句、循环语句或者是单独的{}
都可以被认为是块级作用域。
下面用一些简单的代码演示块级作用域:
js
//if块
if(1){}
//while块
while(1){}
//函数块
function foo(){}
//for循环块
for(let i = 0; i<100; i++){}
//单独一个块
{}
块级作用域简单来说就是代码块内部定义的变量在代码块外部访问不到。并且在该代码块执行结束后变量会被销毁。为什么会出现会计作用域就要先了解一下在没有块级作用域之前有什么问题。其实就是变量提升带来的问题。
变量提升带来的问题
- 变量容易被覆盖掉
- 本应销毁的变量没有被销毁
变量容易被覆盖
在没有块级作用域时我们用 var
定义变量时经常会出现在不经意间变量被覆盖的情况。例如一下实例代码,大家猜猜会输出什么:
js
ar myname = "掘金"
function showName() {
console.log(myname)
if(0) {
var myname = "沸点"
}
console.log(myname)
}
showName()
可以从上图看出执行后输出的是 undefined
,并没有像想象的一样输出"掘金"。原因我们可以借助上篇 JavaScript 在浏览器中的执行机制之"调用栈"中的调用栈执行状态来分析一下。
结合代码和上图的调用栈可以看出有两个 myname
变量,一个在全局执行上下文中,一个在函数执行上下文中。相信作为 资深切图仔
的大家都知道 myname
肯定是使用函数上下文中的变量啦 。因为在执行过程中 JavaScript 优先会从当前的执行上下文中找变量,而由于变量提升导致当前执行上下文中包含了变量 myname
,但是又因为变量提升值是 undefined
所以最后输出的值就是 undfined
。
本应销毁的变量没有销毁
首先来看一下下面的一段代码:
js
function foo() {
for (var i = 0; i < 7; i++) {
}
console.log(i)
}
foo()
结合上面代码和执行结果可以看到输出的是7和预想的不一样,这也是因为变量提升导致的,在创建执行上下文之前变量就被提升了,但是 for
循环结束后变量并没有被销毁所以最后输出的是7。
ES6 是如何解决上述问题的
解决上述问题就该提到我们开头提到的 ES6 引入的 let 和 const
。 关于let 和 const 的用法来简单看一段代码:
js
let a = '掘金'
const b = '沸点'
a = 课程'
b = '小册' // 报错,const声明的不能修改
从上面代码就可以看出用let声明的变量是可修改的,但是用const声明的变量不能修改。不过二者都会生成块级作用域。
接下来两段代码来对比用 var 和用 let、const代码输出结果的区别:
js
// var
function test1() {
var x = 1
if (1) {
var x = 2
console.log(x) // 2
}
console.log(x) // 2
}
js
// let
function test2() {
let x = 1
if (1) {
let x = 2
console.log(x) // 2
}
console.log(x) // 1
}
通过上面两段代码可以看出使用 var 时因为变量提升值被覆盖,两次输出都是2,但是使用 let 生成了块级作用域两次输出结果不一样,在 if 条件内输出的是2,在外部输出的是1。这就非常符合我们日常使用的习惯了,作用域内声明的变量不会影响外面的变量。
JavaScript 是如何支持块级作用域的
在使用过程中可能会出现混用的情况,那 JS 是如何做到既支持变量提升又支持块级作用域的呢?
我们还是从执行上下文入手。前面已经了解过 JavaScript 引擎是通过变量环境实现的函数作用域,那块级作用域是怎么实现的,结合下面的代码和调用栈解析来探究一下:
js
function foo() {
var x = 1
let y = 2
{
let y = 3
var z = 4
let s = 5
console.log(x)
console.log(y)
}
console.log(y)
console.log(z)
console.log(s)
}
foo()
从代码的执行结果可以看出只有变量 s 输出报错,其他输出都有结果,这是因为块级作用域外部访问不到。接下来看一下代码的执行流程。
首先是编译并创建执行上下文:
从上图的调用栈可以看出以下内容:
- 函数内通过 var 声明的变量都在变量环境中
- 通过let 或者 const 声明的变量在编译阶段会被放到词法环境(JavaScript 在浏览器中的执行机制之"调用栈"提到过)中
- 在函数作用域块用let或者const声明的变量没有放到词法环境中
接下来执行到了代码块,变量环境x的值变为1,词法环境的y变为2,如下图所示:
进入函数块时,作用域let或const声明的变量会被存在词法环境的一个单独的区域(小型栈结构),这个区域的变量不会影响变量环境的变量,当执行到作用域内部它们都是独立存在的。
在执行 console.log(x)
时先在词法环境中找 x 的值,从栈顶向下,找到就返回找不到就去变量环境中找。具体执行流程如下:
然后执行结束后函数块内的变量就会从词法环境栈弹出,最终结果如下图所示:
总结
因为 JavaScript 存在变量提升引起变量覆盖、变量不能回收等缺陷,ES6 引入了let和const关键字来结果,之后通过分析调用栈的执行过程,介绍了 JavaScript 引擎是如何同时支持变量提升和块级作用域的。