引言
我把变量提升和函数提升比作日食和月食。日食和月食的形成大家都知道,是由于太阳、地球、月球在运动过程中刚好在一条直线上,月亮刚好在太阳和地球之间形成日食,地球刚好在月亮和太阳之间形成月食。
我们通俗的理解月食会说月亮被天狗吃掉了,而实际是由地球、月球的运行轨迹导致的。我们通俗的理解变量提升会说,变量声明提升到它所在作用域的最开始的部分,而实际是执行上下文运行机制导致的。要想拨开变量提升和函数提升的表象看到内容,我们就需要了解一下执行上下文的过程。
一、执行上下文的2个阶段
当每次调用函数的时候都会创建一个新的执行上下文,然而,在JavaScript解释器内部,对执行上下文的每次调用都有下面的2个阶段。
1. 创建阶段
当函数被调用,在执行里边的代码之前
- 创建作用域链
- 创建参数、变量和函数
- 确定this的值创建完成的执行上下文对象,表示为:
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
2. 代码执行阶段:
赋值,函数的引用以及解析/执行代码
二、创建阶段的详细过程
- 初始化作用链
- 创建variableObject
- (1) 创建arguments对象,检查传入的参数,初始化名称和值,并且创建一个引用的副本
- (2) 扫描上下文中的函数声明
- 每找到一个函数,就以函数名在variableObject中创建一个属性,属性指向函数的内存空间
- 如果variableObject中存在函数名称的属性,则属性的引用值会被覆盖
- (3) 扫描上下文中的变量声明
- 每找到一个变量,就以变量名在variableObject中创建一个属性,属性值初始化为undefined
- 如果variableObject中存在变量名称的属性,不做处理,继续扫描
- 确定上下文中this的值
!理解变量提升和函数提升,扫描上下文中的函数声明和扫描上下文中的变量声明的过程是关键
三、举个例子
下面是很常见的一道面试题:
var a =123;
function f() {
console.log(a);
console.log(b);
var a = 1;
var b = 2;
function a () {}
}
f();
- 当执行方法f的时候会创建一个执行上下文对象,这时executionContextObj为一个空对象
executionContextObj = {}
- 然后进入到创建阶段,上述第(1)步,由于没有参数,executionContextObj创建一个空的arguments数组
executionContextObj = {
arguments: []
}
- 接着执行第(2)步,扫描上下文中函数的声明, 找到了函数 function a () {}, 在variableObject中增加属性a, 值为a函数
executionContextObj = {
arguments: [],
variableObject: {
a: function a () {}
}
}
- 再执行第(3)步,扫描上下文中变量的声明,找到了变量a和变量b, 由于variableObject中已经有a属性了,不做处理;没有b属性,在variableObject中增加属性b, 值为undefined
executionContextObj = {
arguments: [],
variableObject: {
a: function a () {},
b: undefined
}
}
- 再确定this值为window,创建阶段结束
executionContextObj = {
arguments: [],
variableObject: {
a: function a () {},
b: undefined
},
this: window
}
- 最后执行代码
- 输出a的值为:function a () {}
- 输出b的值为:undefined