深刻浅出javascript (3)—— let 和 const

深刻浅出javascript (1)—— 变量提高中咱们知道使用var 声明变量常常会有意想不到的效果,所以在ES6中引入了块级做用域以及 let、const 关键字来规避这种状况。今天咱们就来讲说。javascript

做用域

想要讲明白 JavaScript 的变量提高这个特性,咱们须要先从做用域讲起。java

做用域是一块在程序中定义变量的区域,该区域决定了变量的生命周期。换句话说,做用域就是变量与函数的可访问范围,它控制着变量和函数的可见性和生命周期。segmentfault

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

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

而其余语言基本都支持块级做用域。块级做用域就是使用一对大括号包裹的一段代码,好比函数、判断语句、循环语句,甚至单独的一个{}均可以被看做是一个块级做用域。spa

例以下面的代码都是块级做用域:设计

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

对于块级做用域最重要的就是其代码块内部定义的变量在代码块外部是访问不到的,而且等该代码块中的代码执行完成以后,代码块中定义的变量会被销毁。
JavaScript 语言设计之初并无引入块级做用域的概念,因而把做用域内部的变量统一提高无疑是最快速、最简单的设计,因此才有了 var 的变量提高。指针

变量提高所带来的问题

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

咱们看这种状况:code

var myname = "wens" 
function showName(){ 
    console.log(myname); 
    if(0){ 
        var myname = "leon" 
    } 
    console.log(myname); 
} 
showName()

执行这段代码,打印出来的是 undefined,而并不会像具备块级做用域那样的语音同样打印出来“wens”的字符串。至于为何输出的内容是 undefined ?相信看过我上一篇文章的同窗必定能知道答案对象

2. 本应销毁的变量没有被销毁

接下来咱们再来看下面这段让人误解更大的代码:blog

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

若是咱们使用有块级做用域的语音,在 for 循环结束以后,i 就已经被销毁了,可是在 JavaScript 代码中,i 的值并未被销毁,因此最后打印出来的是 7。

这一样也是由变量提高而致使的,在建立执行上下文阶段,变量 i 就已经被提高了,因此当 for 循环结束以后,变量 i 并无被销毁。

let 和 const

上面咱们介绍了变量提高而带来的一系列问题,为了解决这些问题,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 的做用范围是整个函数,因此在编译阶段,会生成以下的执行上下文:
image.png

从执行上下文的变量环境中能够看出,最终只生成了一个变量 x,函数体内全部对 x 的赋值操做都会直接改变变量环境中的 x 值。

因此上述代码最后经过 console.log(x) 输出的是 2,而对于相同逻辑的代码,有块级做用域的语言最后一步输出的值应该是 1,由于在 if 块里面的声明不该该影响到块外面的变量。
下面咱们使用 let 关键字替换 var 关键字使其具有块级做用域,改造后的代码以下:

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

执行这段代码,其输出结果就和咱们的预期是一致的。

那么,JavaScript 是如何在没有破坏变量提高的状况下还支持块级做用域呢?
下面咱们经过一段代码来讲明:

function foo(){ 
    var a = 1 
    let b = 2 
    { 
        let b = 3 
        var c = 4 
        let d = 5 
        console.log(a) 
        console.log(b) 
    } 
    console.log(b) 
    console.log(c) 
    console.log(d)
}
foo()

读过前面两篇文章的同窗都知道,当执行上面这段代码的时候,JavaScript 引擎会先对其进行编译并建立执行上下文,而后再按照顺序执行代码。可是如今的状况有点不同,咱们引入了 let 关键字这就有了块级做用域。下面是执行过程:

第一步是编译并建立执行上下文:
image.png

经过上图,咱们能够得出如下结论:

  • 函数内部经过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
  • 经过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
  • 在函数的做用域块内部,经过 let 声明的变量并无被存放到词法环境中。

接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就以下图所示:

image.png

从图中能够看出,当进入函数的做用域块时,做用域块中经过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响做用域块外面的变量,好比在做用域外面声明了变量 b,在该做用域块内部也声明了变量 b,当执行到做用域内部时,它们都是独立的存在。

其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个做用域块后,就会把该做用域块内部的变量压到栈顶;看成用域执行完成以后,该做用域的信息就会从栈顶弹出,这就是词法环境的结构。

再接下来,当执行到做用域块中的console.log(a)这行代码时,就须要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,若是在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,若是没有查找到,那么继续在变量环境中查找(一个完整的查找变量和函数的过程会涉及到做用域链,这个咱们在下篇文章会介绍)。

看成用域块执行结束以后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文以下图所示:

image.png

经过上面的分析,想必你已经理解了词法环境的结构和工做机制,块级做用域就是经过词法环境的栈结构来实现的,而变量提高是经过变量环境来实现,经过这二者的结合,JavaScript 引擎也就同时支持了变量提高和块级做用域了。

总结

今天讲解的内容就结束了,下面我来简单总结下今天的内容。

因为 JavaScript 的变量提高存在着变量覆盖、变量污染等设计缺陷,因此 ES6 引入了块级做用域关键字来解决这些问题。

咱们经过对变量环境和词法环境的介绍,分析了 JavaScript 引擎是如何同时支持变量提高和块级做用域的。

下一篇文章咱们来说解一下做用域链。

相关文章
相关标签/搜索