msgbartop
PHP源码分析,Zend引擎分析,Web相关技术研究,Web技术分享–左手代码 右手诗
msgbarbottom

28 May 09 Javascript作用域原理

问题的提出

首先看一个例子:

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

echo();

运行结果是什么呢?

上面的问题, 我相信会有很多人会认为是:

laruence
eve
[脚本出错]

因为会以为在echo中, 第一次alert的时候, 会取到全局变量name的值, 而第二次值被局部变量name覆盖, 所以第二次alert是’eve’. 而age属性没有定义, 所以脚本会出错.

但其实, 运行结果应该是:

undefined
eve
[脚本出错]

为什么呢?

JavaScript的作用域链

首先让让我们来看看Javasript(简称JS, 不完全代表JScript)的作用域的原理: JS权威指南中有一句很精辟的描述: ”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.” 

为了接下来的知识, 你能顺利理解, 我再提醒一下, 在JS中:”一切皆是对象, 函数也是”.

在JS中,作用域的概念和其他语言差不多, 在每次调用一个函数的时候 ,就会进入一个函数内的作用域,当从函数返回以后,就返回调用前的作用域.

JS的语法风格和C/C++类似, 但作用域的实现却和C/C++不同,并非用“堆栈”方式,而是使用列表,具体过程如下(ECMA262中所述):
任何执行上下文时刻的作用域, 都是由作用域链(scope chain, 后面介绍)来实现.
在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性.
在一个函数对象被调用的时候,会创建一个活动对象(也就是一个对象), 然后对于每一个函数的形参,都命名为该活动对象的命名属性, 然后将这个活动对象做为此时的作用域链(scope chain)最前端, 并将这个函数对象的[[scope]]加入到scope chain中.

看个例子:

	var func = function(lps, rps){
		var name = 'laruence';
		........
	}
	func();

在执行func的定义语句的时候, 会创建一个这个函数对象的[[scope]]属性(内部属性,只有JS引擎可以访问, 但FireFox的几个引擎(SpiderMonkey和Rhino)提供了私有属性__parent__来访问它), 并将这个[[scope]]属性, 链接到定义它的作用域链上(后面会详细介绍), 此时因为func定义在全局环境, 所以此时的[[scope]]只是指向全局活动对象window active object.

在调用func的时候, 会创建一个活动对象(假设为aObj, 由JS引擎预编译时刻创建, 后面会介绍),并创建arguments属性, 然后会给这个对象添加俩个命名属性aObj.lps, aObj.rps; 对于每一个在这个函数中申明的局部变量和函数定义, 都作为该活动对象的同名命名属性.

然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined。

然后将这个活动对象做为scope chain的最前端, 并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象, 加入到scope china.

有了上面的作用域链, 在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。

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

	var name = 'laruence';
	function echo() {
		alert(name);
	}

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

	env();

运行结果是:

laruence

结合上面的知识, 我们来看看下面这个例子:

function factory() {
	var name = 'laruence';
	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如下:

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

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

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

注意到, 此时的作用域链中, 并不包含app的活动对象.

在定义intro函数的时候, intro函数的[[scope]]为:

[[scope chain]] = [
{
	name : 'laruence',
	intor : undefined
}, {
	window call object
}
]

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

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

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

所以运行结果是:

I am laruence

现在, 大家对”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”这句话, 应该有了个全面的认识了吧?

Javascript的预编译

我们都知道,JS是一种脚本语言, JS的执行过程, 是一种翻译执行的过程.
那么JS的执行中, 有没有类似编译的过程呢?

首先, 我们来看一个例子:

	<script>
	alert(typeof eve); //function
		function eve() {
			alert('I am Laruence');
		};
	</script>

诶? 在alert的时候, eve不是应该还是未定义的么? 怎么eve的类型还是function呢?

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

而对于函数的定义,是一个要注意的地方:

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

这就是函数定义式和函数表达式的不同, 对于函数定义式, 会将函数定义提前. 而函数表达式, 会在执行过程中才计算.

说到这里, 顺便说一个问题 :

	var name = 'laruence';
	age = 26;

我们都知道不使用var关键字定义的变量, 相当于是全局变量, 联系到我们刚才的知识:

在对age做标识符解析的时候, 因为是写操作, 所以当找到到全局的window活动对象的时候都没有找到这个标识符的时候, 会在window活动对象的基础上, 返回一个值为undefined的age属性.

也就是说, age会被定义在顶级作用域中.

现在, 也许你注意到了我刚才说的: JS在执行每一段JS代码..
对, 让我们看看下面的例子:

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

明白了么? 也就是JS的预编译是以段为处理单元的…

揭开谜底

现在让我们回到我们的第一个问题:

当echo函数被调用的时候, echo的活动对象已经被预编译过程创建, 此时echo的活动对象为:

[callObj] = {
name : undefined
}

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

现在你明白了吧?

Related Posts:

Tags: , , ,

Reader's Comments

  1. |

    从变量和函数的预解析方面来讲可能会容易理解一点~

  2. |

    受教了, 看到一些很新的东西, 谢谢~

  3. |

    yahoo克军对这个搞得比较透彻,js的原型链,听他搞过tech talk

  4. |

    克军可是大牛啊..当初我们一起封闭开发的时候, 就已经领略到他的深厚功底了..

  5. |

    “而当b()在内部调用a()的时候,a()的[[scope]]由:全局活动对象->b的活动对象->a的活动对象组成。”
    这句貌似是有问题的,比如下面这个例子

    var name = ’shankka’;

    var myEchoName = function () {
    document.writeln(name);
    };

    var myEchoCaller = function () {
    var name = ‘cc’;
    myEchoName();
    };

    myEchoCaller();

    运行结果是shankka而不是cc。

    用“当函数被另一个函数调用的时候,this被绑定到全局变量中,而不是被绑定到外部函数的this上”才能解释通

    (没有找到你blog的引用地址,就在评论里写了)

  6. |

    恩,恩,这句是有问题, 这句是我原来的那篇里面直接扣过来的. sorry

  7. |

    [...] 请以:Javascript作用域原理为准. [...]

  8. |

    结果为1//1

    php这是咋的啦?

    请原谅我在这里贴这个问题, 因为我估计你很少看留言板 sorry

  9. |

    代码是
    echo ‘a’==0,’/';
    echo ‘a’==false,’/';
    echo 0==false;

  10. |

    突然看倒答案了,不用大搅了, 谢谢

  11. |

    ;) ~~~

  12. |

    函数的表达式和定义式准确地区别是什么呢?难道只要填充任意一个语句就是定义式?

  13. |

    学习了,http://blog.360.yahoo.com/blog-sOW1QOA9crUyOdXFxOeK4xc-?cq=1&l=171&u=175&mx=191&lmt=5这个是克军的blog吗?

  14. |

    是啊…..

  15. |

    [...] http://www.laruence.com/2009/05/28/863.html [...]

  16. |

    怎么将活动对象加入到作用域链中啊?比如:

    function thisTest(){
    alert(this.value); // 弹出undefined, this在这里指向??
    }

    第2种正确

    function thisTest(){
    alert(this.value);
    }
    document.getElementById(“btnTest”).onclick=thisTest; //给button的onclick事件注册一个函数

    像这样的注册方式可以将window 变成了 button

  17. |

    谁调用,谁就是this, 监听时间是被被监听对象所调用的.

  18. |

    你不是说过this指向当前的活动对象吗?那这样呢
    function thisTest()
    {
    this.userName= ‘outer userName’;
    function innerThisTest(){
    var userName=”inner userName”;
    alert(userName); //inner userName
    alert(this.userName); //outer userName
    }
    return innerThisTest;
    }

    var a = thisTest();
    //我的理解作用域链thisTest->window
    a();
    //作用域链innerThisTest->thisTest->window

    而且我将var 改成 this 会有很大的区别,为什么this里面没有取到var所定义的参数?是应为你说的预编译的时候对标示符解析的原因吗?

  19. |

    @swit1983 啊? 我在哪里说的? 如果有, 那就是我说顺口, 说错了…..sorry, 你可以参看我的最新博文.

  20. |

    你4月9号说的,为什么我的this.userName 不是得到 innerThis中的 var userName 而是 thisTest里面的啊,如果我将innerThis中的 var 改成this又可以了

  21. |

    还有更多看起来很离谱的事情呢。
    比如:

    function test(xxx){
    alert(xxx)
    var xxx = 123;
    function xxx(){
    }
    alert(xxx)
    }
    test(444);

    输出的是 function,123

    函数调用的时候,scope关系好像是这样的:

    enter argumentMap;
    enter functionMap;
    enter varMap;
    apply();
    exitScope();

    function 编译的时候,首先被声明,他升值

  22. |

    @jindw 这个是因为var和function定义在预编译的时候被提前. 而var提前只是占位,计算留在后面执行的过程.
    所以第一次xxx是function, 到第二个的时候, xxx被var xxx=123给改写成了123.

  23. |

    好文

  24. |

    :)受教了

Leave a Comment

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word