JavaScript之作用域原理

原文出处

1.问题的提出

var name='wicle';
function echo(){
    alert(name);
    var name='eve';
    alert(name);
    alert(age);
}
echo();

很多人会认为上面的结果是:

wicle
eve
[脚本出错]

理由是:他们认为,在echo()中,第一次alert的时候,取到全局变量name的值,而第二次值被局部变量name覆盖,所以第二次是‘eve’,而age属性没有定义,所以脚本会出错。
但其实,运行结果是:

undefined
eve
[脚本出错]

2.JavaScript的作用域链

JS权威指南中描述的JS的作用域原理:
JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。
还有一句著名的话:

在JS中,一切皆是对象,函数也是。

在JS中,作用域的概念和其他语言差不多,在每次调用一个函数的时候,就会进入一个函数内的作用域,当从作用域返回以后,就会返回调用前的作用域。
JS的作用域实现并非是C/C++的堆栈方式,而是使用列表,具体如下:
(ECMA262:)
任何执行上下文时刻的作用域,都是由作用域链(scope chain)来实现。
在一个函数被定义的时候,会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性。
在一个函数对象被调用的时候,会创建一个活动对象(也就是一个对象),然后对于每一个函数的形参,都命名为该活动对象的命名属性,然后将这个活动对象作为此时的作用域链的最前端,并将这个函数对象的【【scope】】(里面是函数定义时的作用域链)加入到scope chain中。

//一个例子
var func = function (lps,rps){
    var name = 'wicle';
}
func();

在执行func的定义语句的时候,会创建一个这个函数对象的【【scope】】属性(内部属性,只有JS引擎可以访问,但FF的几个引擎(SpiderMonkey和Rhino)提供了私有属性parent来访问它),并将这个【【scope】】属性,链接到定义它的作用域链上,此时因为func定义在全局环境,所以此时的【【scope】】属性只是指向全局活动对象window active object。
在调用func的时候,会创建一个活动对象(设为aObj,由JS编译引擎预编译时刻创建),并创建arguments属性,然后会给这个对象添加两个命名属性aObj.lps,aObj.rps;对于每一个在这个函数中声明的局部变量和函数定义,都作为该活动对象的同名命名属性。
然后将调用参数复制给形参,对于缺少的调用参数,赋值为undefined。
然后将这个活动对象作为scope chain的最前端,并将func的【【scope】】属性所指向的,定义func时候的顶级活动对象,加入到scope chain。
有了上面的作用域链,在发生标识符解析的时候,就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回,找不到,就认为标识符没有被定义。

因为函数对象的【【scope】】属性是在定义一个函数的时候决定的,而非调用的时候,所以如下面的例子:

var name = 'wicle';
function echo(){
    alert(name);
}
function env(){
    var name = 'eve';
    echo();
}
env();

运行结果是

wicle

现在看下面这个例子

function factory(){
    var name='wicle';
    var intro=function(){
        alert('I am '+name);
    }
    return intro;
}
function app(para){
    var name =para;
    var func = factory();
    func();
}
app('eve');

当调用app的时候,scope chain是由{window活动对象(全局)}->{app的活动对象}组成。
在刚进入app函数体时,app的活动对象有一个arguments属性,有两个值为undefined的属性:name和func,和一个值为’eve‘的属性para

[[scope chain]]=[
{
    para:'eve',
    name:unfefined,
    func:undefined,
    arguments:[]
},{
    window call object
}
]

当调用进入factory的函数体的时候,此时的factory的scope chain为:

[[scope chain]]=[
{
    name:unfefined,
    intro:undefined,
},{
    window call object
}
]

注意到,此时的作用域链中,并不包含app的活动对象。
在定义intro函数的时候,intro函数的【【scope】】为:

[[scope chain]]=[
{
    name:'wicle',
    intro:undefined,
},{
    window call object
}
]

从factory函数返回后,在app体内调用intro的时候,发生了标识符解析,此时的scope chain 是:

[[scope chain]]=[
{
    intro call object
},{
    name:'wicle',
    intro:undefined,
},{
    window call object
}
]

因为scope chain中,并不包含factory活动对象,所以,name标识符解析的结果应该是factory活动对象中的name属性,也就是’wicle‘
所以,运行结果是

I am wicle

3.JavaScript的预编译

JS是一种脚本语言,JS的执行过程,是一种翻译执行的过程,
那么,JS的执行中,有没有类似编译的过程呢?
例子:

<script>
alert(typeof eve);//function
function eve(){
    alert ("I am wicle.");
}
</script>

在js中,是有预编译过程的,JS在执行梅一段代码之前,都会首先处理var关键字和function定义式(函数定义式和函数表达式)
如上文所说,在调用函数执行之前,会首先创建一个活动对象,然后搜寻这个函数中的局部变量定义和函数定义,将变量名和函数名都作为这个活动对象的同名属性,对于局部变量定义,变量的值会在真正执行的时候才计算,此时只是简单的赋值为undefined
而对于函数的定义,有一个要注意的地方:

<script>
    alert(typeof eve);//结果:function
    alert(typeof walle);//结果:undefined
    function eve(){//函数定义式
        alert('I am wicle');
    };
    var walle = function(){//函数表达式
//函数体       
    }
    alert(typeof walle);//结果:function
</script>

对于函数定义式,会将函数提前执行,而函数表达式,会在执行过程中才计算。

顺便:

var name = 'wicle';
age=20;

不使用var 关键字定义的变量,相当于全局变量,联系到刚才的知识:
在对age做标识符解析的时候,因为是写操作,所以当找到全局的window活动对象的时候,都没有找到这个标识符的时候,会在window活动对象的基础上,返回一个值为undefined的age属性。
也就是说,age会被定义在顶级作用域中。

???说的什么意思??

下面再看一个例子

<script>
    alert(typeof eve);//结果:undefined
</script>
<script>
    function(){
        alert('I am wicle');
    }
</script>

也就是说,JS的预编译是以段为处理单元的。

揭开谜底

回到最开始的问题
当echo函数被调用的时候,echo的活动对象已经被预编译过程创建,此时echo的活动对象为:

[callObj]={
    name:undefined
}

当第一次alert的时候,发生了标识符解析,在echo的活动对象中找到了name属性,所以这个name属性完全遮挡了全局活动对象中的name属性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值