首先,为什么某些函数以及变量在没有被声明以前就可以被使用,javascript引擎内部在执行代码以前到底做了些什么?这里,想信大家都会想到,变量声明提前这个概念;
但是,以下我要讲的是,声明提前的这个原理;
首先,“执行上下文”,不要和作用域混淆了,这不是同一个概念。
js代码运行的环境分为以下几个:
全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境==>全局上下文
函数级别的代码 - 当执行一个函数时,运行函数体中的代码。==>函数上下文
Eval的代码 - 在Eval函数内运行的代码。
每个运行环境下的代码都会产生一个执行上下文,此时的执行上下文则包含了,变量,函数声明,函数参数;
接着,我要说下另一个家伙,VO(variable object),他叫做变量对象,用于存储执行上下文中的变量、函数参数、函数声明,因此结合上一句话,执行上下文的具体表现形式就是VO,变量对象;
接下来看下一个例子:
function test(a,b){
var c=10;
function d(){};
var e = function() _e(){};
(function x(){});
b=20;
} test(10);
分析以上这段代码,我们举这个例子来讲讲函数的执行上下文。函数执行上下文会分为2个阶段:初始化阶段,执行阶段
1、初始化阶段
这里vo对象初始化有一个顺序规则:函数参数传入->函数声明-->变量声明;
好,首先函数参数传入,会保存到VO里面,这里a 传入为10,而b未传入,为undefined,因此vo里面的a:10,b:undefined;
接着,函数声明,也会被保存到vo里面,(这里就是函数声明提前的原因,在代码还没被执行的时候,执行上下文vo里面已经存在该函数了),但是,这里有个注意点,就是如果函数名和上一步的函数参数传入同名,则该函数会覆盖上一步传入的参数;因此这里d:<ref to func 'd'>
最后,变量声明,一样也会被保存至vo,var c = 10;这里,c = 10是赋值操作。我们这个时候是处于vo初始化阶段,因此,这个时候,vo只保存c : undefined,e:undefined;(这里的注意点是,如果函数名和上一步函数声明的函数名重名,则该变量被忽略);
到这里,变量初始化阶段结束,这时候的执行上下文的vo对象如下:
VO(test) = {
a:10,
b:undefined,
d:<ref to func 'd'>,
c:undefined,
e:undefined
}
2、执行阶段(此阶段变量对象称为AO)
这个阶段就非常简单了,这个阶段则是对上一个阶段初始化完成的vo对象里面的属性进行赋值,结果如下:
AO(test) = {
a:10,
b:20,
d:<ref to func 'd'>,
c:10,
e:function _e(){}
}
为了更好的解释这个过程,再来一个例子:
function foo(x,y,z){
function x(){};
var x;
console.log(x);
};
foo(100);
首先,函数执行上下文vo对象初始化阶段:
foo()函数传入参数为100,x-->100;
函数声明 function x(){},故函数名与foo函数入参一致,采用覆盖,则 x--><ref to func 'x'>
变量x声明,由于和function x (){}同名,故采用忽略;
此时:
vo(foo)={
x:<ref to func 'x'>
} ;
执行阶段也没有变;故打印出来的x是function (){}
若将以上代码稍作修改:
function foo(x,y,z){
function x(){};
var x=1;
console.log(x);
};
foo(100);
此情况下的初始化阶段和上面例子是完全一致的;
唯一不同的是,在执行阶段,,x 被赋值为1,故:
AO(foo)={
x:1
} ;
打印出来的值是,1;
总结,这部分的内容比较难以理解;但是这个内容可以对变量和函数声明提前这个知识点理解有所帮助;
即其原理即是在执行上下文的初始阶段,变量和函数都被初始化存入vo对象,执行阶段的时候,解析器可以在vo对象中找得到对应的变量或者函数;