调用堆栈(二)-执行上下文和变量对象
变量提升
JS是单线程的语言,执行顺序肯定是顺序执行,但是JS引擎并不是一行一行地分析和执行程序,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。
1、变量声明提升
变量声明提升我们在JS基础部分已经说过,再简单看一下例子吧:
console.log(a); // undefined
var a = 10;
console.log(a); // 10
这里第一行代码并不会报错,因为var声明的变量会进行提升到顶部,实际上的执行过程是:
var a ; // 声明变量a
console.log(a);
a = 10;
console.log(a)
2、函数声明提升
定义函数也有两种方法:
- 函数声明: function foo() {};
- 函数表达式: var foo = function() {};
第二种函数表达式的声明方式更像是给一个变量foo赋值一个匿名函数。
那这两种方式有什么区别呢?
eg1:
console.log(f1) //f f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式
可以看到,使用function函数声明的方式会将整个函数都提升到作用域的最顶部,因此打印出来的是整个函数。
而使用函数表达式声明则类似于变量声明提升,将var f2提升到了顶部并赋值undefined。
eg2:
console.log(f1);
f1();
function f1() {
console.log('1')
}
console.log(f2);
f2();
var f2 = function() {
console.log('2')
}

我们可以看到f1()在function f1() {}之前,但是却可以正常执行;而f2()却会报错,原因在上面的例子里也介绍了,因为在调用f2时,f2还只是undefined并没有被赋值为 一个函数,因此会报错;因此在使用变量声明函数的时候,一定要在声明之后才能调用。
eg3:
foo(); // foo2
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
通过这个例子我们可以看到,如果同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。
eg4:
foo(); // foo2
var foo = function() {
console.log('foo1')
}
foo(); // foo1, foo重新赋值
function foo() {
console.log('foo2')
}
foo(); // foo1
从这个例子中我们可以看到,函数声明优先级高于变量声明,也就是两种声明方式都存在时,使用function关键字的函数声明会先声明,使用变量的是后声明,所以这个例子也就会出现了第一次执行时打印foo2,而当执行到后面两次的foo()函数时,使用变量声明的foo函数覆盖了使用function 声明的foo函数。
执行上下文栈
执行上下文栈在调用堆栈(一)中已经进行了一些介绍,因为JS引擎创建了很多的执行上下文,所以JS引擎创建了执行上下文栈(Execution context stack, ECS)来管理执行上下文。
当JS初始化的时候会向执行上下文栈压入一个全局执行上下文,我们用globalContext表示它,并且只有当整个应用程序结束的时候,执行栈才会被清空,所以程序结束之前,执行栈最底部永远有个globalContext。
ESStack = [ //使用数组模拟栈
globalContext
]
下面看两段代码:
代码1:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope();
代码2:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f
}
checkscope()();
这两段代码执行结果是一样的,但是它们的执行上下文栈的变化不一样。
第一段代码:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
第二段代码:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
函数上下文
变量对象(VO):也就是variable object,创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中。
活动对象(AO):也就是activation object,进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活了。
活动对象和变量对象的区别在于:
- 1、变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
- 2、当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。
执行过程
首先来看看一个执行上下文(EC)被创建和执行的过程:
1、创建阶段
* 创建变量、参数、函数arguments对象;
* 创建作用域链;
* 确定this的值
2、执行阶段
变量赋值、函数引用,执行代码。
进入执行上下文
这个时候还没有执行代码
此时的变量对象会包括(如下顺序初始化):
- 1、函数的所有形参(only函数上下文):没有实参,属性值设为undefined。
- 2、函数声明:如果变量对象已经存在相同的属性,则完全替换这个属性。
- 3、变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。
看个例子吧:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1)
对于上面的代码,这个时候的AO是
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
形参arguments这时候已经有赋值了,但是变量还是undefined,只是初始化的值。
代码执行
这个阶段会顺序执行代码,修改变量对象的值,执行完成后AO如下
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression 'd'
}
总结如下:
1、全局上下文的变量对象初始化是全局对象
2、函数上下文的变量对象初始化只包括Arguments对象
3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
4、在代码执行阶段,会再次修改变量对象的属性值
另外需要注意的是:
ES6支持新的变量声明方式 let 、const,规则与 var 完全不同,他们是在上下文的执行阶段开始执行,避免了变量的提升带来的一系列问题。
参考:
https://m528964214.blog.csdn.net/article/details/87909673
https://muyiy.cn/blog/1/1.2.html#参考

浙公网安备 33010602011771号