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属性.
现在你明白了吧?

87 Comments

  1. mary
    mary October 29, 2019

    thank you~

  2. 羊
    November 28, 2017

    “’因为scope chain中,并不包含factory活动对象. 所以, name标识符解析的结果应该是factory活动对象中的name属性, 也就是’laruence’. ” 这句话不理解。
    另外,“在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。”
    中“逆向”一词有歧义。 让我误解为从scope chain 最后一个元素查。实际是顺序。
    谢谢作者。

  3. lyn
    lyn July 17, 2017

    看了好多遍,懵懵,最后发现用词非常之准确。
    1

  4. hurtworld hack
    hurtworld hack September 6, 2016

    Both way, these are game titless tto view to see tweaks on formulation annd at anny time-prettier graphics, but not
    so a lot reinvention and startling new concepts.

  5. qeesung
    qeesung April 1, 2016

    通俗易懂,好文好文!

  6. phping
    phping March 24, 2016

    写得不错,有些地方一时还是难以消化,在实际开发中慢慢消化吧。

  7. […] Javascript 是一种脚本语言,在脚本执行时,是要经过预编译的。关于“JavaScript的作用域链”和“Javascript的预编译”可以学习鸟哥的Javascript作用域原理。 […]

  8. 潮有起落
    潮有起落 August 27, 2015

    下面的问题已明白,衷心感谢。

  9. 潮有起落
    潮有起落 August 27, 2015

    func(){
    name=”one”;
    alert(name);
    }
    这个变量name会在那个活动的对象中创建同名属性?

  10. 木心
    木心 June 4, 2015

    因为scope chain中,并不包含factory活动对象. 所以, name标识符解析的结果应该是factory活动对象中的name属性, 也就是’laruence’.
    //这里这一句话是不是打错了哦 应该是scope chain中并不包含app活动对象.所以,name标识符解析的结果应该是factory活动对象的name属性,也就是’laruence’.
    还是受教了!
    如果是我理解错了请指正!

  11. […] P73 4.2 执行环境及作用域鸟哥:JavaScript作用域原理JavaScript 开发进阶:理解 JavaScript […]

  12. […] 这里引用鸟哥的描述 在调用函数执行之前, 会首先创建一个活动对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个活动对象的同名属性, 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined. […]

  13. jzz15
    jzz15 May 18, 2015

    写的真的太好了!!!我看完JavaScript高级程序设计那本书里面的作用域链之后再来看这个感觉更加巩固了理解.

  14. jzz
    jzz May 18, 2015

    写的真的太好了!!!我看完JavaScript高级程序设计那本书里面的作用域链之后再来看这个感觉更加巩固了理解.

  15. wintercoder
    wintercoder May 12, 2015

    感谢,看了几遍收获挺大~

  16. Eric Miao
    Eric Miao March 4, 2015

    引用:
    在定义intro函数的时候, intro函数的[[scope]]为:
    “`
    [[scope chain]] = [
    {
    name : ‘laruence’,
    intor : undefined
    }, {
    window call object
    }
    ]
    “`
    问题:
    在这里为什么会有一个 `intor:undefined`?
    不应该是
    “`
    [[scope chain]] = [
    {
    name : ‘laruence’,
    }, {
    window call object
    }
    ]
    “`
    嘛?

  17. deep
    deep May 30, 2013

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

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

  19. Anonymous
    Anonymous February 19, 2013

    讲得太好了,谢谢

  20. Praise
    Praise December 2, 2012

    非常透彻的分析

  21. billfeller
    billfeller August 18, 2012

    有问题请教:
    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。

  22. casperchen
    casperchen August 17, 2012

    有问题请教:
    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’
    求指导。

  23. differui
    differui July 12, 2012

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

  24. wkylin
    wkylin November 19, 2011

    __parent__你确定是这个吗??

  25. 雪候鸟
    雪候鸟 October 24, 2011

    @henry fixed, thanks 🙂

  26. xdf
    xdf October 23, 2011

    factory函数返回undefined

  27. 袖之欢
    袖之欢 October 22, 2011

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

  28. 小哥
    小哥 August 28, 2011

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

  29. raintree
    raintree July 29, 2011

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

  30. henry
    henry July 29, 2011

    并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象, 加入到scope china.
    一个小拼写错误:scope china 应该是 scope chain

  31. 雪候鸟
    雪候鸟 March 30, 2011

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

  32. 小哥
    小哥 March 29, 2011

    鸟哥,看了5、6遍,有些地方不明白,不知道能不能帮我解答下。
    function t1(){
    function t2(){
    }
    }
    像这样形式的函数,定义t2函数的时候是在函数t1里面,那是不是t1的[[scope]]是连接到t1的作用域里面?如果不是,那是连接到window下?

  33. 豪情
    豪情 March 24, 2011

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

  34. narco
    narco September 29, 2010

    拜读^_^

  35. Wxl
    Wxl September 21, 2010

    受教了
    顺道请教个问题
    怎么访问私有的 活动变量?
    比如
    函数内的活动变量 应该存放在一个集合里吧
    function a(){
    var test = ‘test’;
    function _get(name){
    return name;
    }
    return function(){
    _get(“test”);
    }
    }()

  36. palgerrard
    palgerrard May 3, 2010

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

  37. 马克龙
    马克龙 November 10, 2009

    :)受教了

  38. Anonymous
    Anonymous October 19, 2009

    好文

  39. 雪候鸟
    雪候鸟 September 8, 2009

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

  40. jindw
    jindw September 8, 2009

    还有更多看起来很离谱的事情呢。
    比如:
    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 编译的时候,首先被声明,他升值

  41. swit1983
    swit1983 September 8, 2009

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

  42. 雪候鸟
    雪候鸟 September 8, 2009

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

  43. swit1983
    swit1983 September 8, 2009

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

  44. 雪候鸟
    雪候鸟 September 7, 2009

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

  45. swit1983
    swit1983 September 7, 2009

    怎么将活动对象加入到作用域链中啊?比如:
    function thisTest(){
    alert(this.value); // 弹出undefined, this在这里指向??
    }
    第2种正确
    function thisTest(){
    alert(this.value);
    }
    document.getElementById(“btnTest”).onclick=thisTest; //给button的onclick事件注册一个函数
    像这样的注册方式可以将window 变成了 button

  46. laruence
    laruence June 5, 2009

    是啊…..

  47. hiwi
    hiwi June 5, 2009

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

  48. sky
    sky May 29, 2009

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

  49. toms
    toms May 28, 2009

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

  50. toms
    toms May 28, 2009

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

  51. toms
    toms May 28, 2009

    结果为1//1
    php这是咋的啦?
    请原谅我在这里贴这个问题, 因为我估计你很少看留言板 sorry

  52. shankka
    shankka May 27, 2009

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

    • 雪候鸟
      雪候鸟 May 27, 2009

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

  53. cc0cc
    cc0cc May 26, 2009

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

    • 雪候鸟
      雪候鸟 May 26, 2009

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

  54. Fdream
    Fdream May 25, 2009

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

    • 雪候鸟
      雪候鸟 May 25, 2009

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

Comments are closed.