Press "Enter" to skip to content

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 chain.

有了上面的作用域链, 在发生标识符解析的时候, 就会逆向查询当前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属性.

现在你明白了吧?

74 Comments

  1. Eric Miao
    Eric Miao 2015-03-04

    引用:
    在定义intro函数的时候, intro函数的[[scope]]为:
    “`
    [[scope chain]] = [
    {
    name : ‘laruence’,
    intor : undefined
    }, {
    window call object
    }
    ]
    “`

    问题:
    在这里为什么会有一个 `intor:undefined`?
    不应该是
    “`
    [[scope chain]] = [
    {
    name : ‘laruence’,
    }, {
    window call object
    }
    ]
    “`
    嘛?

  2. deep
    deep 2013-05-30

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

  3. […] 可以参看Javascript作用域原理这篇文章进行更深入的理解,这里就不进行详述了。这里只对其中一个容易被大家忽视的一个特性进行说明。 […]

  4. Anonymous
    Anonymous 2013-02-19

    讲得太好了,谢谢

  5. billfeller
    billfeller 2012-08-18

    有问题请教:
    var list = [1,2,3];
    function list(){
    if(typeof list === ‘undefined’){
    list = [];
    }
    console.log(list.length);
    }
    list(); //list is not a function
    console.log(list[0]); //1

    输出结果如上所示,猜测是不是function list的定义被提到最前(变量前),然后list = [1,2,3]又覆盖了它的定义,导致list()的时候出错

    解释:解析器在代码执行前,通过函数声明提升读取并将函数声明和变量声明添加到执行环境(其实也就是当前活动对象)中,然后执行list = [1,2,3]的赋值操作,所以当你调用list()时,其实list是[1,2,3]数组,故不可以当作函数调用。你后面的验证是一样的道理,

    做了如下验证:

    console.log(list.toString()); //打印出方法里的内容
    console.log(name); //undefined
    function list(){
    //内容略
    }
    var name = ‘casper’

    解释:当代码执行之前,解析器先读取函数声明list和变量声明name,此时name还没有进行赋值运算,所以仍是undefined,所以在你输出name到控制台时name为undefined。

  6. casperchen
    casperchen 2012-08-17

    有问题请教:
    var list = [1,2,3];
    function list(){
    if(typeof list === ‘undefined’){
    list = [];
    }
    console.log(list.length);
    }
    list(); //list is not a function
    console.log(list[0]); //1

    输出结果如上所示,猜测是不是function list的定义被提到最前(变量前),然后list = [1,2,3]又覆盖了它的定义,导致list()的时候出错

    做了如下验证:

    console.log(list.toString()); //打印出方法里的内容
    console.log(name); //undefined
    function list(){
    //内容略
    }
    var name = ‘casper’

    求指导。

  7. differui
    differui 2012-07-12

    问下如何在firefox中使用私有定义的属性__parent__访问函数的[[scope]]属性呢?

  8. wkylin
    wkylin 2011-11-19

    __parent__你确定是这个吗??

  9. xdf
    xdf 2011-10-23

    factory函数返回undefined

  10. 袖之欢
    袖之欢 2011-10-22

    想请问一下,为什么从factory函数返回以后,仍然是”intro:undefined”呢~~

  11. 小哥
    小哥 2011-08-28

    请问一下,那这个执行函数才会创建的活动对象是不是就是指 ‘上下文’?

  12. raintree
    raintree 2011-07-29

    要是能用普通的定义和解释方式多好啊

  13. henry
    henry 2011-07-29

    并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象, 加入到scope china.

    一个小拼写错误:scope china 应该是 scope chain

  14. 雪候鸟
    雪候鸟 2011-03-30

    @小哥 没明白你的问题, 你是问t2的作用域和t1的作用域连的关系?

  15. 小哥
    小哥 2011-03-29

    鸟哥,看了5、6遍,有些地方不明白,不知道能不能帮我解答下。

    function t1(){
    function t2(){

    }
    }

    像这样形式的函数,定义t2函数的时候是在函数t1里面,那是不是t1的[[scope]]是连接到t1的作用域里面?如果不是,那是连接到window下?

  16. 豪情
    豪情 2011-03-24

    帅哥,你的变量名也就罢了,方法名太不容易理解了。
    app,echo,让不熟悉php的朋友很难记住。
    为何不用简单的sayName and sayHello 来实例呢?

  17. Wxl
    Wxl 2010-09-21

    受教了
    顺道请教个问题
    怎么访问私有的 活动变量?
    比如
    函数内的活动变量 应该存放在一个集合里吧

    function a(){

    var test = ‘test’;

    function _get(name){
    return name;
    }

    return function(){

    _get(“test”);
    }
    }()

  18. palgerrard
    palgerrard 2010-05-03

    研究的很深入!非常感谢你的分享~

  19. Anonymous
    Anonymous 2009-10-19

    好文

  20. 雪候鸟
    雪候鸟 2009-09-08

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

  21. jindw
    jindw 2009-09-08

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

    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. swit1983
    swit1983 2009-09-08

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

  23. 雪候鸟
    雪候鸟 2009-09-08

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

  24. swit1983
    swit1983 2009-09-08

    你不是说过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所定义的参数?是应为你说的预编译的时候对标示符解析的原因吗?

  25. 雪候鸟
    雪候鸟 2009-09-07

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

  26. swit1983
    swit1983 2009-09-07

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

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

    第2种正确

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

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

  27. laruence
    laruence 2009-06-05

    是啊…..

  28. hiwi
    hiwi 2009-06-05

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

  29. sky
    sky 2009-05-29

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

  30. toms
    toms 2009-05-28

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

  31. toms
    toms 2009-05-28

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

  32. toms
    toms 2009-05-28

    结果为1//1

    php这是咋的啦?

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

  33. shankka
    shankka 2009-05-27

    “而当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的引用地址,就在评论里写了)

    • 雪候鸟
      雪候鸟 2009-05-27

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

  34. cc0cc
    cc0cc 2009-05-26

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

    • 雪候鸟
      雪候鸟 2009-05-26

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

  35. Fdream
    Fdream 2009-05-25

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

    • 雪候鸟
      雪候鸟 2009-05-25

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

Leave a Reply

Your email address will not be published. Required fields are marked *