什么是作用域
程序的执行,离不开作用域,也必须在作用域中才能将代码正确的执行。
所以作用域到底是什么,通俗的说,可以这样理解:作用域就是定义变量的位置,是变量和函数的可访问范围,控制着变量和函数的可见性和生命周期。
而JavaScript中的作用域,在ES6之前和ES6之后,有两种不同的情况。
ES6之前,JavaScript作用域有两种:函数作用域和全局作用域。
ES6之后,JavaScript新增了块级作用域。
作用域的特性
在JavaScript变量提升的讨论中,我们其实是缺少了一个作用域的概念的,变量提升其实也是针对在同一作用域中的代码来说的。
对编译器的了解,让我们明白,对于一段代码【var a = 10】变量的赋值操作,其实是包含了两个过程:
1、变量的声明和隐式赋值(var a = undefined),这个阶段在编译时
2、变量的赋值(a = 10),这个阶段在运行时
先看一下如下代码:
var flag = true; if(flag) {
var someStr = 'flag is true';
} function doSomething() {
var someStr = 'in doSomething';
var otherStr = 'some other string';
console.log(someStr);
console.log(flag);
} doSomething(); for(var i = 0; i < 10; i++) {
console.log(i);
} console.log(i); {
var place = 'i do not want to be visited';
}
那么这一些代码在编译之后,执行之前,根据变量提升的机制,我们可以知道应该是下面这个样子:
function doSomething() { // 函数优先提升
// 提升隐式赋值
var someStr = undefined;
var otherStr = undefined; someStr = 'in doSomething';
otherStr = 'some other string'; console.log(someStr);
console.log(flag);
} // 隐式赋值和提升
var flag = undefined;
var someStr = undefined;
var i = undefined;
var place = undefined; flag = true; if(flag) {
someStr = 'flag is true';
} for(i = 0; i < 10; i++) {
console.log(i);
} doSomething(); console.log(i); {
place = 'i do not want to be visited';
}
因为变量的提升特性,以及无块级作用域的概念,所以代码中在同一个作用域中变量和函数的定义,在编译阶段都会提升到顶部。
通过上述代码,我们大体上可以得出作用域的特性:
第一、内部作用域和外部作用域是嵌套关系。外部作用域完全包含内部作用域。
第二、内部作用域可访问外部作用域的变量,但是外部作用域不能访问内部作用域的变量,(链式继承,向上部作用域查找)。
第三、变量提升是在同一个作用域内部出现的。
第四、作用域用于编译器在编译代码时候,确定变量和函数声明的位置。
块级作用域
上述代码,在ES6+的环境中运行,也是和ES6之前是相同的结果,但是ES6不是引用了块级作用域吗,为什么大括号块内的代码还是会出现和之前一样的编译方式呢?
那么,ES6中的块级作用域到底是什么?
let & const
利用var定义的变量,具有提升的性质,可能会影响代码的执行结果。
这是var定义变量的缺陷,那么如何规避这种缺陷呢?在ES6中,设计出来了let和const来重新定变量。
但是,由于JavaScript标准定义的非常早,1995年5月JavaScript方案定义,1996年微软提供了JavaScript解决方案JScript。而网景公司为了同微软竞争,神情了JavaScript标准,于是,1997年6月第一个国际标准ECMA-262便颁布了。
C语言标准化的过程却是将近二十年后才颁布。
所以,我们以后设计的语言既要兼容var也要有自己的块级作用域,让var和let以及const在引擎做到兼容。
所以,我们定义块级作用域的标准,只能从定义变量的方式入手,而不是直接一个{}块就可以解决。
先让我们看一下下面代码:
var name = 'someName'; function doSomething(){
console.log(name);
if(true) {
var name = 'otherName';
}
} doSomething(); 结果:undefined
产生这个结果的原因是我们函数内部的变量提升,覆盖了外部作用域的变量,也就是说,其实打印出来的值是doSomething函数中的变量声明的值。
但是这样却并不符合块级作用域的预期,如果有许多类似代码,理解起来也会相当困难。如果将代码用ES6方式改写:
let name = 'someName'; function doSomething(){
console.log(name);
if(true) {
let name = 'otherName';
}
} doSomething(); 结果:'someName'
从运行结果看,我们真正的做到了块级作用域应该有的效果,那么let和const又是如何支持块作用域的呢?
执行上下文
先想想一下JavaScript中的一个作用域两个执行上下文中的编译过程中的环境:
变量环境:编译阶段var声明存放的位置(一个大对象)。
词法环境:我们代码书写的位置,也是let和const的初始化位置(代码按词法环境顺序执行,按照{}划分的栈结构)。
而在编译阶段,我们将var定义的变量全都在编译过程在变量环境初始化为undefined,但是用let和const定义的变量,其实他们并未在变量环境初始化,而是在词法环境初始化,也就是执行代码位置初始化。
词法环境的特点:按照{}划分的一个栈结构。
变量查找方式
JavaScript中变量查找的方式:沿着词法环境的栈顶向下查找,找不到的变量去变量环境中查找,这样就形成了先查找代码块中的变量,再查找提升之后的变量环境,这样就形成了块级作用域的概念。
上面的代码形成两种环境的情况如下:
一、全局环境的执行上下文
变量环境:函数声明function doSomething() { ... }
词法环境栈:执行到let name = 'someName';让语句name = 'someName'入栈。
二、doSomething的执行上下文(被全局环境包裹)
变量环境:无
词法环境栈情况:执行到let name = 'otherName',语句的时候,name = 'other'才会入栈;
JavaScript代码执行方式
执行doSomething的时候,还未执行let name = 'otherName',所以,此时doSomething的词法环境中并未有name = 'otherName',这个时候查找,只能向外部作用域查找(全局作用域)
此时查找到全局作用域name = 'someName'所以此时就打印了someName
代码接着执行到了if语句内部,才会将name = 'otherName'入栈,但是此时因为语句已经执行完毕,所以也就无关痛痒了。
JavaScript也就通过这种方式,实现了块级别作用域。
总结
JavaScript中的作用域总的来说,分为块级作用域、函数作用域、全局作用域。
而每个作用域都会创建自身的执行上下文,每一个执行上下文又分为了变量环境和词法环境两部分。
块级作用域的实现,其实是根据定义的let和const声明的变量放置在词法环境栈中这一特性来实现。
这一特性被社区的人叫做‘暂时性死区’,但是在JavaScript标准中并未有这个概念。
只有理解了作用域的概念,才能真正明白JavaScript的执行机制,才能减少我们因为变量定义等发生的错误。