作用域(scope)
作用域指的是程序中定义变量的区域,该位置决定了变量的生命周期。
在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。
变量提升
变量提升指的是在某一个作用域下,代码在编译阶段就会当前作用域下的变量都放到了当前作用域中,无论是在作用域哪个位置申明的,在整个作用域下都可以被访问到。例如:
function test () {
console.log(a)
var a = 2
}
test() // undefined
如果在test函数作用域下变量a没有被变量提升,那在变量申明之前打印变量a的话肯定会报错:not defined。
这其实也很奇怪,我们都没有申明的变量居然可以访问到,这势必会造成一定程度的混乱。在举个例子:
var name = 'juejin'
function setName () {
if (0) {
var name = ''
}
console.log(name)
}
setName() // undefined
看这个例子似乎更不可思议了,一段并不会执行的代码居然影响了输出的结果。
为什么会输出undefined也很好理解。这里存在两个作用域:全局作用域和函数setName内部作用域。在这两个作用域下都申明和赋值了变量name。而我们打印语句的位置是在函数作用域下,那肯定优先取最近的变量值,但是从执行的角度来说,函数作用域下并未对变量name进行赋值,正常情况下会输出全局作用域定义的值,但结果却是undefined,这就是变量提升带来的影响,在编译阶段已经把作用域下的所有变量都提取到了当前作用域,可供当前作用域下任何地方使用。
变量提升还可能会带来一个问题,原本应该被回收的变量却没有被回收销毁。
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i) // 7
}
foo()
执行结果返回7,按正常逻辑,变量i在for循环执行之后应该是要被销毁的,输出应该是未定义变量,但在foo函数作用域下变量i仍然是存在的。
块级作用域
在前面我们说过,JavaScript在ES6之前只存在两种作用域,分别是全局作用域和函数作用域。只有在ES6中开始应用了块级作用域的。
是在什么背景下新增的或解决什么问题的呢?我们具体来看看。
ES6如何解决变量提升问题
简单说就是声明变量的关键字发生了改变。在JS中,ES6之前,所有变量都是通过var关键字来申明,这也就是为什么说JS是弱类型语言,它申明时不区分是数字类型还是字符串、或布尔值。
ES6新增了两个申明变量的关键字let和const。这两个关键字申明的变量是块级作用域有效的。举个例子:
function _declare () {
let a = 100
if (true) {
console.log(a) // a is not defined
let a = 200
console.log(a) // 200
}
console.log(a) // 100
}
_declare()
由此可见,if代码块内外的变量a是相互间不影响的。并且在if代码块内第一个变量打印报错了,说明也不存在变量提升,要不然打印值就是undefined。
let和const关键字最大的区别在于:
const申明的关键字不允许再修改了,特别是对于基本数据类型的变量申明要注意,如果被const申明后,再赋值,则会报错Assignment to constant variable.,也就是说const适用于申明常量。
ES6如何支持块级作用域
我们先看一段代码:
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a) // 1
console.log(b) // 3
}
console.log(b) // 2
console.log(c) // 4
console.log(d) // d is not defined
}
foo()
我们先来说明结果:
变量a仅在函数foo内定义,输出1;变量b不仅在foo函数作用域内申明,而且在函数内块级作用域内也申明了,由于是let关键字申明的,其实在两个作用域中是互不影响的,所以输出3;函数块外就输出2;变量c虽然定义在函数块作用域内,但是由关键字var申明的,不存在块级作用域,所以输出4;变量d是仅在函数块级作用域内通过let关键字申明,仅在函数块内有效,所以会报错未定义。
但在解释器内部是如何区分这些变量的呢?
首先,对于同作用域下用var关键字申明的变量,都会存入变量环境中,如变量a和c,都是通过var关键字申明的,不管是在函数作用域内还是函数块作用域内;
其次,在函数作用域内通过let或const关键字申明的变量会被放入词法环境中,如变量b;
最后,在函数作用域内的块级作用域下通过let或const关键字申明的变量会被放到词法环境的另一个存储空间内,如变量b和d。并且这个变量b和刚才函数作用域下的变量b不在同一个存储空间,是分开的。
如下图所示:
作用域链
词法作用域
词法作用域指的是作用域由代码中函数的声明的位置来决定,所以说词法作用域是静态作用域。说更清楚一点就是词法作用域与你编写的程序位置有关,与函数调用栈没有关系。
作用域链是由词法作用域决定的。
作用域链
function bar() {
console.log(myName) // juejin
}
function foo() {
var myName = "xitu"
bar()
}
var myName = "juejin"
foo()
我们执行这段代码,控制台打印的结果是juejin,这是不是与我们的期望不符合。变量myName在全局作用域和foo函数作用域都申明赋值了,并且bar函数是在foo函数中调用,myName变量虽然在bar函数中未申明,那它会继续往上找,应该是取foo函数内的申明变量值呢,返回的结果应该是xitu,而非juejin。
但实际并不是这样。
还记得我们上面的一句话:作用域是由词法作用域决定的。
这就决定了函数bar的上一级作用域是全局作用域,而不是调用它的foo函数作用域。bar函数作用域中myName没有申明,那就到全局作用域中继续查找,全局作用域中定义的变量值是juejin,所以结果返回的就是juejin。
块级作用域中变量查找
我们先来看段代码:
function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test) // 1
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
根据以上分析的作用域链的查找规则其实就很容易分析出来,输出的是全局作用域下的test变量值1。因为它所处的块级作用域没有test变量申明,再沿着作用域链往上查找,bar函数内部也没有,继续往上找就是全局作用域了。
对于作用域链,我们只要记住两点:
- 原作用域只有两种:全局作用域和函数作用域,ES6新增了块级作用域
- 作用域链是由词法作用域决定的,而非函数调用栈