全面了解JavaScript的作用域链
JavaScript的作用域链
这是一个非常重要的知识点了,了解了JavaScript的作用域链的话,能帮助我们理解很多‘异常'问题。
下面我们来看一个小例子,前面我说过的声明提前的例子。
varname='Skylor.min'; functionecho(){ alert(name); varname='mm'; alert(name); alert(age); } echo();
对于这个例子,没有接触过这方面的时候,第一反应是会纠结下,这第一个的name,到底调用全局变量的name,还是函数内部的name呢,如果调用全局的,可是函数内部也用定义和赋值啊,如果调用函数内部的局部变量的话,那么他的值是mm吗?还是引用全局的'Skylor.min'呢?
于是这个小例子就会有这样的错误答案:
Skylor.min
mm
[脚本出错]
其实不然,知道函数内的提前说明,就知道这是不正确的。
undefined
mm
[脚本出错]
应该是这样的,那到底为什么是这个答案呢,提前声明这又是什么呢?一切的一切,涉及到JavaScript的作用域链。
原理
首先来说说,JavaScript的作用域的原理:
在JavaScript权威指南中有一句很精辟的描述:JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。
另外在JavaScript中有个很重要的概念,那就是:在JavaScript中,一切皆对象,函数也是。
在JS中,作用域的概念和其他语言差不多,在每次调用一个函数的时候,就会进入一个函数内的作用域,当从函数返回以后,就返回调用前的作用域
JS的语法风格和C/C++类似,但作用域的实现却和C/C++不同,并非用“堆栈”方式,而是使用列表,具体过程如下(ECMA262中所述):
- 任何执行上下文时刻的作用域,都是由作用域链(scopechain,后面介绍)来实现
- 在一个函数被定义的时候,会将它定义时刻的scopechain链接到这个函数对象的[[scope]]属性
- 在一个函数对象被调用的时候,会创建一个活动对象(也就是一个对象),然后对于每一个函数的形参,都命名为该活动对象的命名属性,然后将这个活动对象做为此时的作用域链(scopechain)最前端,并将这个函数对象的[[scope]]加入到scopechain中.
看个例子吧:
varfunc=function(lps,rps){
varname='Skylor.min';
........
}
func();
在执行func的定义语句的时候,会创建一个这个函数对象的[[scope]]属性(内部属性,只有JS引擎可以访问,但FireFox的几个引擎(SpiderMonkey和Rhino)提供了私有属性__parent__来访问它),并将这个[[scope]]属性,链接到定义它的作用域链上(后面会详细介绍),此时因为func定义在全局环境,所以此时的[[scope]]只是指向全局活动对象windowactiveobject.
在调用func的时候,会创建一个活动对象(假设为aObj,由JS引擎预编译时刻创建,后面会介绍),并创建arguments属性,然后会给这个对象添加俩个命名属性aObj.lps,aObj.rps;对于每一个在这个函数中申明的局部变量和函数定义,都作为该活动对象的同名命名属性.
然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined。
然后将这个活动对象做为scopechain的最前端,并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象,加入到scopechain.
有了上面的作用域链,在发生标识符解析的时候,就会逆向查询当前scopechain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。
注意到,因为函数对象的[[scope]]属性是在定义一个函数的时候决定的,而非调用的时候,所以如下面的例子:
varname='Skylor.min'; functionecho(){ alert(name); } functionenv(){ varname='mm'; echo(); } env();
他的运行结果是:Skylor.min
结合上面的知识,我们来看看下面这个例子,还记得那句JavaScript权威指南中的经典,JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。
functionfactory(){ varname='Skylor.min'; varintro=function(){ alert('Iam'+name); } returnintro; } functionapp(para){ varname=para; varfunc=factory(); func(); } app('mm');
当调用app的时候,scopechain是由:{window活动对象(全局)}->{app的活动对象}组成.
在刚进入app函数体时,app的活动对象有一个arguments属性,其他俩个值为undefined的属性:name和func.和一个值为'mm'的属性para;
此时的scopechain如下:
[[scopechain]]=[ { para:'mm', name:undefined, func:undefined, arguments:[] },{ windowcallobject } ]
当调用进入factory的函数体的时候,此时的factory的scopechain为:
[[scopechain]]=[ { name:undefined, intor:undefined },{ windowcallobject } ]
注意到,此时的作用域链中,并不包含app的活动对象.
在定义intro函数的时候,intro函数的[[scope]]为:
[[scopechain]]=[ { name:'Skylor.min', intor:undefined },{ windowcallobject } ]
从factory函数返回以后,在app体内调用intor的时候,发生了标识符解析,而此时的sopechain是:
[[scopechain]]=[ { introcallobject },{ name:'Skylor.min', intor:undefined },{ windowcallobject } ]
因为scopechain中,并不包含factory活动对象.所以,name标识符解析的结果应该是factory活动对象中的name属性,也就是'Skylor.min'.
所以运行结果是:IamSkylor.min
至此,完整的一个运行流程,很清晰的能读懂“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被运行的作用域里。”这句话讲的是什么了。
为了解释上面的一些问题,还得说说JavaScript的预编译。
JavaScriptの预编译
预编译,学过C等的我们都知道,可是问题来了,JavaScript是脚本语言,JavaScript的执行过程是一种翻译执行的过程,那在JavaScript的执行中,有没有类似编译的过程呢?
如果不是很确定,先通过一个例子:
alert(typeoffun);//function
functionfun(){
alert('IamSkylor.min');
};
这时候弹出来的是?-----我去,是“IamSkylor.min”然而这时为什么呢,为啥不是undefined呢。
恩,对,在JS中,是有预编译的过程的,JS在执行每一段JS代码之前,都会首先处理var关键字和function定义式(函数定义式和函数表达式).
如上文所说,在调用函数执行之前,会首先创建一个活动对象,然后搜寻这个函数中的局部变量定义,和函数定义,将变量名和函数名都做为这个活动对象的同名属性,对于局部变量定义,变量的值会在真正执行的时候才计算,此时只是简单的赋为undefined.
而对于函数的定义,是一个要注意的地方:
alert(typeoffun);//结果:function alert(typeoffn);//结果:undefined functionfun(){//函数定义式 alert('IamSkylor.min'); }; varfn=function(){//函数表达式 } alert(typeoffn);//结果:function
这就是函数定义式和函数表达式的不同,对于函数定义式,会将函数定义提前.而函数表达式,会在执行过程中才计算.
说到这里,顺便说一个问题:
varname='Skylor.min';
age=25;
我们都知道不使用var关键字定义的变量,相当于是全局变量,联系到我们刚才的知识:
在对age做标识符解析的时候,因为是写操作,所以当找到到全局的window活动对象的时候都没有找到这个标识符的时候,会在window活动对象的基础上,返回一个值为undefined的age属性.
也就是说,age会被定义在顶级作用域中.
现在,也许你注意到了我刚才说的:JS在执行每一段JS代码之前,都会首先处理var关键字和function定义式(函数定义式和函数表达式).
对,让我们看看下面的例子:
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。