接着执行上下文来总结,前面已经说了,执行上下文的生命周期分为创建阶段和执行阶段。而创建阶段是创建变量对象,建立作用域链和确定this的指向,这些对学习JS都很重要。
这篇重点是在变量对象(Variable Object)。
变量对象的创建,依次经历了以下的几个历程
建立arguments对象。检查当前上下文中的参数,建立该对象下的属性和属性值。
检查当前上下文的函数声明,也就是使用function关键字声明的函数。在该变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果该函数名的属性已经存在,那么该属性将会被新的引用所覆盖(即使用新值替换旧值,也解释JS为什么没有函数重载)。
检查当前上下文中的变量声明(es6以前就是指使用 var 声明的变量),每找到一个变量声明,就在该变量对象中以变量名创建一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性不会被修改。
这下面有一段代码,先来看看。
function foo() {
console.log('foo');
}
var foo = 20;
console.log(foo);// 20
不是说foo变量不会覆盖函数的吗?为什么最后还是输出了20呢?
!!!注意:上面的规则只是针对于变量对象的创建过程,而变量对象的创建只是执行上下文的创建阶段。而foo = 20
是执 行上下文的执行阶段中运行的,输出结果是 20
并不奇怪。
对比一下这段代码
console.log(foo); // ƒ foo() { console.log('foo'); }
function foo() {
console.log('foo');
}
var foo = 20;
这里为什么是输出 ƒ foo() { console.log('foo'); }
呢?很简单,因为执行到 console.log(foo)
的时候,此时foo
的值还是保存着指向函数foo在内存地址的引用而已,所以打印了函数foo,而不是20。
// 执行顺序
// 首先将所有函数声明放入变量对象中
function foo() { console.log('foo') }
// 其次将所有变量声明放入变量对象中
// 但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
// var foo = undefined; 该次赋值无效
// 然后开始执行阶段代码的执行
console.log(foo); // function foo
foo = 20;
这也是变量提升的本质,从变量对象的创建过程中可以看出,function声明会比var声明优先级高。
// demo01
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
我们从执行上下文开始理解。代码开始执行时, 全局上下文会入栈,然后遇到test()时就开始它的执行上下文的创建。
// 创建过程
testEC = {
// 变量对象
VO: {},
// 作用域链
scopeChain: {}
}
// VO 为 Variable Object的缩写,即变量对象
VO = {
//注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,做了这样的处理
arguments: {...},
foo: // 表示函数foo的地址引用
a: undefined
}
!!!注意:未进入执行阶段之前,变量对象中的属性都不能访问!当进入执行上下文的执行阶段之后,变量对象转变为了活动对象,里面的属性才可以被访问,然后开始进行执行阶段的操作。
变量对象和活动对象有什么不同呢?答案是 变量对象和活动对象都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的上下文中(即当前上下文)的变量对象,才会变成活动对象。
// 执行阶段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: ,
a: 1,
this: Window
}
再来看看下面的一个例子
// demo2
function test() {
console.log(foo);
console.log(bar);
var foo = 'Hello';
console.log(foo);
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
// 创建阶段
VO = {
arguments: {...},
foo: ,
bar: undefined
}
// 这里有一个需要注意的地方,因为var声明的变量当遇到同名的属性时,会跳过而不会覆盖
// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: ,
this: Window
}
全局上下文的变量对象
以浏览器为例,全局对象为window。全局上下文有一个特殊的地方,它的变量对象就是window对象。全局上下文中的this,也很特殊,this也是指向window。
// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
VO: Window,
scopeChain: {},
this: Window
}
除了上面提到的,全局上下文的生命周期,与程序的生命周期一致。只有程序运行不结束,全局上下文就会一直存在。其它所有的上下文环境,都能直接访问全局上下文的属性。