在JavaScript中,我们肯定需要声明变量和函数,可是JS解析器是如何找到这些变量的呢?我们还得对执行上下文有一个进一步的了解。
我们已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。
-
创建阶段:执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
-
代码执行阶段:创建完成后执行代码,会完成变量赋值,函数引用,以及执行其他代码。
执行上下文生命周期
变量对象(Variable Object)
变量对象的创建,依次经历了以下几个过程。
- 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
- 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
- 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
看下下面这样的场景,既然变量声明的foo遇到函数声明的foo会跳过,可是为什么最后foo的输出结果仍然是被覆盖了?
function foo() { console.log('function foo') }
var foo = 20;
console.log(foo); // 20
因为上面的三条规则仅仅适用于变量对象的创建过程。也就是执行上下文的创建过程。而foo = 20(赋值而不是定义)是在执行上下文的执行过程中运行的,输出结果自然会是20。对比下例。
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
// 真正的执行顺序为:
// 1.首先将所有函数声明放入变量对象中
function foo() { console.log('function foo') }
// 2.其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
// var foo = undefined;
// 然后开始执行阶段代码的执行
console.log(foo); // function foo
foo = 20;
根据这个规则,理解变量提升就变得十分简单了。因为创建过程都是会先去寻找是否有声明,赋值则是第二步执行时期才会发生的事。同时我们也能得出函数声明的优先级要大于var 声明的结论。
// demo01
function test() {
console.log(a);
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
在上例中,我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示
// 创建过程
testEC = {
// VO 为 Variable Object的缩写,即变量对象
VO: {
arguments: {...}, //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
foo: <foo reference> // 表示foo的地址引用
a: undefined
},
scopeChain: {}//作用域链,下一篇讲
}
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。
这样,如果再面试的时候被问到变量对象和活动对象有什么区别,就又可以自如的应答了,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。
// 执行阶段
VO -> AO // Active Object
AO = {
arguments: {...},
foo: <foo reference>,
a: 1,
this: Window
}
demo1真正的执行顺序就变成如下这样:
function test() {
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();
// 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();
真正的执行顺序:
function test() {
function foo() {
return 'hello';
}
//var foo;变量foo声明的同名属性已存在,不覆盖前面同名属性的值
//如果是函数声明再次声明同名属性,则覆盖前面同名属性的值
var bar;
console.log(foo);//ƒ foo() {return 'hello';}
console.log(bar);//undefined
foo = 'Hello';
console.log(foo);//Hello
bar = function () {
return 'world';
}
}
test();
全局上下文的变量对象
以浏览器中为例,全局对象为window。
全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window。
// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
VO: Window,
scopeChain: {},
this: Window
}
除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。
扩展:声明变量有哪些方式
address2='333';
var address3='111'
console.log(window.address2);//333
console.log(window.address3);//undefined 是变量对象里的属性,而不是window下的直接属性
直接使用:num=1;并不是声明了一个全局变量,而是创建了一个全局对象(window)的属性。
var 声明变量,全局环境下会存储到window的变量对象里,是window全局变量的属性,而不是全局对象直接的属性,所以window.address3访问不到
console.log(myName);//undefined
console.log(webName);//undefined
console.log(address);//address is not defined
var myName='Nick';
var webName;
address;
我们都知道var声明的变量有变量提升的功能,既可以声明之前访问到。由上面例子可以看出,如果address也是变量,就不会报未定义的错。
明确一点,在ES规范明确规定只有用var、let或者const声明的才是变量或者常量。
不使用var创建的address是全局对象的一个属性,属性并没有前置现象(变量提升),所以会报错。
既然webName是变量,那为什么可以使用window.webName访问此变量,好像webName也是一个属性。其实并不是,变量在执行上下文中的变量对象(VO)中存储,是变量对象的一个属性,在全局执行上下文中,全局对象恰好是变量对象,于是全局作用域中的变量可以使用全局对象window访问。这也是为什么delete无法删除使用var声明的变量,可以删除不是用var声明的"变量"的原因(eval执行上下文除外)。
由于变量声明自带不可删除属性,比较var num = 1 跟 num = 1,前者是变量声明,带不可删除属性,因此无法被删除;后者为全局变量的一个属性,因此可以从全局变量中删除。
网友评论