我们来分析下面这个例子,当运行到console.log(test)
时,输出的结果是什么?
function bar() {
var myName = "bar"
let test1 = 100
if (1) {
let myName = "bar bar"
console.log(test)
}
}
function foo() {
var myName = "foo"
let test = 2
{
let test = 3
let myName = "foo foo"
bar()
}
}
var myName = "global"
let test = 1
let myAge = 10
foo()
执行到第6行 console.log(test) 时的调用栈如下:
变量提升
JavaScript 代码执行过程中,需要先做变量提升,之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined。在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
var myName = "global"
这行代码实际是分两步执行,先声明再赋值。
var myName //声明部分
myName = "global" //赋值部分
编译阶段 myName 被存放到变量环境中,变量默认值被设为 undefined.
所以没执行到赋值这一步时,myName的值都为 undefined.
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
全局作用域、函数作用域和块级作用域
在ES6之前,ES的作用域只有两种:全局作用域和函数作用域,ES6通过词法环境实现块级作用域。通过 let 或者 const 声明的变量,在编译阶段会被存放到词法环境中。
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。
如上图所示,bar执行上下文的词法环境中,有栈1和栈2两个栈。
let myName = "bar bar"
被存放到栈1,
let test1 = 100
被存放到栈2。
作用域链
作用域链是通过词法作用域来确定的.,在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。沿着词法环境的栈顶向下查询,如果在词法环境中找到就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。
分析例子
console.log(test) 在bar的执行上下文中,bar的词法环境中有栈1和栈2两个栈,栈1和栈2都没有找到test变量,接着去bar的变量环境3中查找,3中也没找到test变量,接着通过outer找到全局执行上下文,在全局执行上下文的词法环境中,栈4保存了test变量,所有test变量的实际值是1。
所以,查找test的顺序为:
1 - > 2 -> 3 -> 4
在全局执行上下文中的词法环境中找到test变量,该值为1。