在JavaScript中,我们肯定不可避免的需要声明变量和函数,可是JS解析器是如何找到这些变量的呢?我们还得对执行上下文有一个进一步的了解。
在上一篇文章中,我们已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。
-
创建阶段
在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。 -
代码执行阶段
创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。
从这里我们就可以看出详细了解执行上下文极为重要,因为其中涉及到了变量对象,作用域链,this等很多人没有怎么弄明白,但是却极为重要的概念,它关系到我们能不能真正理解JavaScript。在后面的文章中我们会一一详细总结,这里我们先重点了解变量对象。
变量对象(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;
// 上栗的执行顺序为
// 首先将所有函数声明放入变量对象中
function foo() { console.log('function 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()的执行上下文开始理解。全局作用域中运行test()
时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示
// 创建过程
testEC = {
// 变量对象
VO: {},
scopeChain: {}
}
// 因为本文暂时不详细解释作用域链,所以把变量对象专门提出来说明
// VO 为 Variable Object的缩写,即变量对象
VO = {
arguments: {...}, //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理
foo: <foo reference> // 表示foo的地址引用
a: undefined
}
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。
这样,如果再面试的时候被问到变量对象和活动对象有什么区别,就又可以自如的应答了,他们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。
// 执行阶段
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();
// 创建阶段
VO = {
arguments: {...},
foo: <foo reference>,
bar: undefined
}
// 这里有一个需要注意的地方,因为var声明的变量当遇到同名的属性时,会跳过而不会覆盖
// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>,
this: Window
}
需要结合上面的知识,仔细对比这个例子中变量对象从创建阶段到执行阶段的变化,如果你已经理解了,说明变量对象相关的东西都已经难不倒你了。
全局上下文的变量对象
以浏览器中为例,全局对象为window。
全局上下文有一个特殊的地方,它的变量对象,就是window对象。而这个特殊,在this指向上也同样适用,this也是指向window。
// 以浏览器中为例,全局对象为window
// 全局上下文
windowEC = {
VO: Window,
scopeChain: {},
this: Window
}
除此之外,全局上下文的生命周期,与程序的生命周期一致,只要程序运行不结束,比如关掉浏览器窗口,全局上下文就会一直存在。其他所有的上下文环境,都能直接访问全局上下文的属性。
网友评论
// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>,
this: Window
}
这里应该已经转换为活动对象AO了~
if(false){
console.log(a());
function a(){
console.log('true');
}
}
a();
像这种情况 是有点问题,感觉说清楚 会好一点
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
输出不应该是 foo() { console.log('function foo') }吗?怎么直接输出 function foo了?
首先是变量函数对象的创建阶段,此时,所有的‘=’赋值都不会进行,先创建函数声明的对象,在创建var声明的对象,此时var声明的对象名称与已有函数名称一样的话会跳过,跳过的意思是就当什么事情都没发上,其余的var声明的对象值都为undefined。
因此,在demo1中,函数执行之前只有foo函数变量和a undefined变量,然后开始执行,当执行到第四行时,才开始给a赋值。
在demo2中,在执行之前只有foo函数变量和var bar变量,然后执行到第四行时才给foo重新赋值为‘hello’,执行到第六行值给bar 赋值为函数。
建议不懂的同学可以把console.log放在不同位置试一试。
在函数被调用(激活)后,JavaScript解释器会构造一个当前函数的执行上下文对象,同时创建VO,然后根据上下文填充VO,最后把上下文对象压入栈内存空间执行函数。是这样的过程吗?
function foo() {
return 2;
}
var a;
console.log(a);
console.log(foo());
a = 1;
}
test();经过解析以后为什么先var a呢 这时候是不是相当于a是undefined;还有就是为什么给var a赋值是在最后 这个有点没看懂
function test() {
console.log(str);
console.log(foo());
var str = 'Hello s';
console.log(str);
function foo() {
return 'hello f';
}
}
test();
```
输出结果为:
undefined
hello f
hello s
变量没有提升,函数提升了。
提醒作者,函数名和变量名不要取相同的导致歧义。
console.log("评论里的小伙伴,你们都不会用markdown缩进吗?看着真难受!");
```
alert("test!");
```
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>
}
这个框里各个参数到底什么意思?<bar reference>又是什么意思,被你整糊涂了。如果你是参考外文的,建议先自己理解清楚再来介绍啊。
1. 创建作用域链;
2. 创建活动对象: 创建argument对象 -> 函数声明 -> 变量声明;
3. 将活动对象的指针 push 到作用域链。
4. 初始化活动对象: this、argument和命名参数的值 -> 变量对象;
这完全是我个人臆想且没有出处,和波老师的VO、AO有所不同,但似乎也能说明问题,是否有错误和忽视,请波老师指正。
console.log(foo);
var foo = 'Hello';
function foo() {
return 'hello';
}
console.log(foo);
}
test();
输出结果:
function foo() {
return 'hello';
}
Hello
谁能给我讲一下,为什么第二个console输出的不是函数体了?
function test() {
console.log(foo);
function foo() {
return 'hello';
}
var foo = 'Hello';
console.log(foo);
}
test();
把函数声明和变量定义位置换一下也不行,输出结果不变,为什么?
function test(){
function foo(){
return 'hello';
};
var foo;
var bar;
console.log(foo); // undefined
console.log(bar); // undefined
foo='hello';
console.log(foo); // hello
bar=function(){
return 'world';
}
};
test();
</script>
demo2 执行顺序是这样的吗 ?
var greet = 'hello';
alert(greet);
function greet(){
}
alert(greet);
}
t3(null);
--------------------------------------------
创建过程:
t3EC = {
//变量对象
VO:{}
}
VO = {
//分析参数,建立该对象下的属性和属性值
arguments:{greet:null},
//分析函数声明
greet : <greet reference>
//分析变量声明,找到"var greet",但greet已经存在,所以在此跳过
}
----------------------------------------------
//执行过程
VO->AO
AO = {
arguments:{greet:null},
//原来的greet是指向一个函数引用的地址,但此时被'hello'覆盖了
greet:'hello'
}
波老师,我这样分析对么
function foo(){
console.log(1);
}
var foo=function(){
console.log(2);
}
foo();//2
按照上面的理论函数优先于变量,那这个不应该输出的是1吗?
function test3(){
function foo(){
return "hello";
}
var foo;
var bar;
console.log(foo);
console.log(bar);
foo="Hello";
console.log(foo);
bar=function(){
return "word";
}
}
test3();
感觉有点问题啊,要这样的顺序,第一个console.log(foo)应该是underfined啊,而且console.log() 什么时候才执行?
有点不太懂在fn和innerFoo都指向同一个变量时,
bar()函数中调用fn()时,
//fn()函数执行上下文入栈
//?此处到底是fn()执行上下文入栈还是innerFoo()执行上下文入栈?
以及在创建变量对象与活动对象时,
只有fn会有属性跟属性值还是两者都?
var fn = null;
function foo(){
var a = 2;
function innerFoo(){
console.log(a);
}
fn = innerFoo; //将innerFoo的引用赋值给全局变量中的fn
}
function bar(){
fn();//此处保留innerFoo的引用
}
foo();
bar();
//执行上下文出入栈
//global上下文入栈
//执行代码foo(),foo()函数执行上下文入栈
//foo()执行上下文出栈
//bar()函数执行上下文入栈
//fn()函数执行上下文入栈
//?此处到底是fn()执行上下文入栈还是innerFoo()执行上下文入栈?
//fn()执行上下文出栈
//bar()执行上下文出栈
//直到网页关闭global上下文出栈
//代码执行分为两个阶段:编译器编译、执行代码
//执行代码阶段完成执行上下文创建
//执行上下文创建分为两个阶段:上下文创建阶段、代码执行阶段
//创建过程
//fooEC = {
// VO:{},//变量对象
// scopeChain:{},//作用域链
// this:{} //this的指向
// }
//其中VO为变量对象
//VO = {
// arguments:{..},//参数
// a: undefined,
// innerFoo: <innerFoo reference>, //表示地址引用
//innerFoo与fn指向都一个地址该如何处理??
// fn: <fn reference>
//}
//执行阶段
//VO->AO 进入执行阶段,变量对象转变为活动对象
//AO = {
// arguments:{...},
// a: 2,
// innerFoo: <innerFoo reference>,
// fn: <fn reference>
//}
function setName (obj){
obj.name = "Nicholas";
obj = new Object(); //函数内部新实例化一个 obj 对象
obj.name = 'Greg';
console.log(obj.name);
}
var person = new Object();
setName(person);
console.log(person.name)
console.log(obj.name) //obj is not defined
此处在函数内部新实例化的obj为什么在函数外面访问不到呢?
function(){
obj = new Object()
a = 7
}
在函数外面可以访问到 a ,但访问不到 obj。 函数内部写的 a = 7 是全局的,为什么同样方式写的 obj 是局部的? 这是什么原因造成的?我在这一点上比较疑惑
var t = function(){
console.log(1);
}
function t(){
console.log(2);
}
t();
var声明并赋值 > function声明 > formal parameter > 预定义变量 > var声明不赋值 > 函数外部作用域的其他所有声明
function test(a){
// function声明 > formal parameter
/*console.log(a);
function a() {
return '1';
}*/
// formal parameter > 预定义变量
var a;
console.log(a);
// 预定义变量 > var声明不赋值
console.log(arguments);
// var arguments;
// var声明不赋值 > 函数外部作用域的其他所有声明
console.log(test);
var test;
}
test(100);
function foo() {
return 'hello';
}
在变量提升时,会在变量对象中以【函数名】建立一个属性,属性值为指向该函数所在内存地址的引用。
那么,对象字面量方法申明的函数
var bar = function () {
return 'world';
}
由于创建时是无名函数,变量对象中的新建的属性名该是什么呢?
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
谢谢你的进阶系列,写的通俗易懂,我想问的是,在我这个例子中,为什么在if语句里面的声明也提前了呢?
var foo;
function foo(){console.log(111)}
console.log(foo)
}
test(4) // function foo(){console.log(111)}
****************************
function test(foo=7){
var foo;
function foo(){console.log(111)}
console.log(foo)
}
test(4) //4
************************
第一个demo,波同学可不可以解释一下这种情况吗? 按照波同学的意思,参数arguments在创建阶段应该是优先级最高的,那么执行test(4)时, foo被赋值为“4”的这一步是在Context的执行阶段进行,还是在创建阶段进行呢?如果是在执行阶段,那这个算是赋值操作吧,理应会覆盖原本函数foo的声明吧?
第二个demo更疑惑,如果用了ES6的默认值,最终的输出值就只会是4
console.log(a);
console.log(b); //执行时这儿就会报b is not defined
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();
b is not defined ,说明如果在AO中没有b,在程序中添加上var b;(不赋值)时,console.log(b)就会显示为undefined,在AO中有b这个变量,只不过值是undefined
console.log(foo());
console.log(bar);
var foo = 'Hello';
console.log(foo());
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
波波,如果我把第二个例子改成这样。提示:foo is not a function 这个原因是??
是允许同名的变量和function,同时存在么?
(function(i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
})(i);
}
可否理解成
AO1 = {
arguemnts: {
i: 1
}
}
setTimeout(function timer() {
console.log(AO1.arguments.i);
}, AO1.arguments.i * 1000)
AO2 = {
arguemnts: {
i: 2
}
}
setTimeout(function timer() {
console.log(AO2.arguments.i);
}, AO2.arguments.i * 1000)
...
var a = 1; var a = 10;第二个a声明跳过,但执行阶段赋值是10对吗?
如果是属性相同的函数声明和变量声明,函数声明优先级最高,所以执行阶段前将会是持有函数引用,但是由于赋值操作在执行阶段,最后总会被重新赋值,覆盖原先的函数引用
波波大神我这样解释对吗?
- 变量未初始化(未赋值)就 调用该变量 的时候,*创建阶段,function声明优于var 声明*
1. 若存在 function 属性,则调用该函数体(同名则覆盖);若不存在则 undefined
- 变量初始化就 调用该变量 的时候,*执行阶段,var 声明优于function声明*
1. 若存在 var 属性,优先执行,同名则覆盖。
2. 若不存在var属性,存在 function 属性,则执行 function 属性函数体,否则为undefined
老师,这个地方不是很懂。
funtion test(){
var a = 1;
var a ;
console.log(a);//1
}
而处于不同作用域(不同执行上下文)时,变量会从新声明:
var a = 1;
funtion test(){
var a;
console.log(a);//undefined
}
对否?
1.变量提升,就是奖变量的声明提升到该变量所属的作用域顶部,但是变量值不会提升,只有在实际赋值的时候才会有值。
console.log(a);//undefined
var a =1;
console.log(a);//1
2.函数提升,分为声明式函数( function test() )和表达式函数( var test = funtion()),而函数提升只针对于声明式函数,这是是为什么很多时候我们可以通过如下方式调用函数,不会出错:
test();
function test(){
console.log(1);
}
而通过这种方式会提示函数未定义:
test();
var test = function(){
console.log(1)
}
function foo(){
return "hello";
}
var foo;
var bar;
console.log(foo);
console.log(bar);
foo="Hello";
console.log(foo);
bar=function(){
return "word";
}
}
test3();
最后一个例子是否变成了这样,感觉理解起来还是时间,基础不好
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>
}
按以上的执行阶段来说
demo2 里的 console.log(bar) 结果为啥是undefind?
return 'world';
}
//创建阶段:
function () {
return 'world';
}
//地址指针:x000001
var bar = undefined;
//执行阶段:
var bar = x000001
return 'world';
}
想问下,这个有var变量声明,又有function函数声明,在创建变量的时候,该如何理解?