本文主要介绍JS执行上下文相关的内容,理解了JavaScript的执行上下文才能更好地理解JavaScript语言本身以及该语言一些特性,如变量提升、作用域和闭包。
一、作用域
1.1 作用域
作用域是指程序中变量定义的区域,该位置决定了变量的生命周期。通俗地说,作用域就是变量与函数的可访问范围,作用域控制着变量和函数的可见性和生命周期。
JavaScript在设计之初并未想过这门语言会如此受欢迎,所以只是按照最简单的方式来设计,只保留全局作用域和函数作用域。
全局作用域中对象和变量在任何地方都能访问,其生命周期同页面的生命周期。
函数作用域是在函数内部定义的变量或函数,只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
1.2 变量提升
JavaScript在执行前会先进行编译,编译阶段会将变量和函数存放在执行上下文的变量环境中,值设为undefined。编译完成进入执行阶段,使用变量时从变量环境中读取,在执行变量的赋值代码前,读取该变量时值均为默认值 undefined。
变量提升给初学者带来了很多疑惑,有如下问题:
变量容易在不被察觉的情况下被覆盖掉
var myname = "JavaScript";
function showName{
console.log(myname); // undefined
if(0){
var myname = "Go"
}
console.log(myname);
}
showName;
本应销毁的变量没有被销毁
function foo{
for (var i = 0; i < 7; i++) {
}
console.log(i); // for循环结束后i变量没有被销魂
}
foo;
1.3 块级作用域
为了解决变量提升等问题、打造JavaScript为企业级的开发语言,ES6引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。
我们已经知道 JavaScript 引擎是通过变量环境实现函数级作用域的,那么 ES6 又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?
我们来看个例子:
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 关键字,let 关键字会创建块级作用域,那么 let 关键字是如何影响执行上下文的呢?
接下来我们就来一步步分析上面这段代码的执行流程。
第一步是编译并创建执行上下文,执行上下文如下图所示:
![361bfbd6199a828236718a5013c19215.png](https://i-blog.csdnimg.cn/blog_migrate/dc27afa6f12996273f53f6ebf55986f8.jpeg)
从上图可以看出:
函数内部通过 var声明的变量,在编译阶段会被存放到变量环境中。
通过 let声明的变量,在编译阶段会存放到词法环境中。
在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中。
第二步是执行代码,当执行到函数代码块里面时(第4行),当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:
![35f6b88f6d208885106825e0491fe20a.png](https://i-blog.csdnimg.cn/blog_migrate/2d3b433f045c9a139bc5769ca49e09f3.jpeg)
从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。
其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或者 const 声明的变量。
再接下来,当执行到作用域块中的 console.log(a) 这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示:
![3f6f23dc41d81b60d3cba6416204a854.png](https://i-blog.csdnimg.cn/blog_migrate/6a8313d12bd31e73cfea4d93a1976e8c.jpeg)
总结一下,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。