当前位置:编程学习 > JAVA >>

举例详细说明javascript作用域、闭包原理以及性能问题


这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存,关系到闭包,关系到javascript运行机制。关系到功能,关系到性能。

文章内容主要参考自《High Performance JavaScript》,这本书对javascript性能方面确实讲的比较深入,大家有空都可以尝试着阅读一下,我这里有中英电子版,需要的话QQ317665171或者QQ邮箱联系。

复习,笔记,更深入的理解。

欢迎拍砖指正。

作用域:

下面我们先搞明白这样几个概念:

  • 函数对象的[[scope]]属性、ScopeChain(作用域链)
  • Execution Context(运行期上下文)、Activation Object(激活对象)

[[scope]]属性:

javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。

该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。例如(例子引用自《High Performance JavaScript高性能javascript》):

 

       function add(num1, num2){

                     var sum = num1 + num2;

                     return sum;

                     }

      

 

	image
		图 1

当add函数被创建时,函数所在的全局作用域的全局对象被放置到add函数的作用域链([[scope]]属性)中。我们可以从图1中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this,window,document以及全局对象中的add函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问window(this),访问全局变量,访问函数自身的原因。当然还有函数作用域不是全局的情况,等会儿我们再讨论。

Execution Context(运行期上下文)、Activation Object(激活对象):

(前天看了老罗的演讲,老罗说过年的时候给全公司的人每人发一台电冰箱,要给校舍的所有的厕所门上都安上新锁,保证童鞋们能有个真正隐私的地方。)

var total = add(5, 10);

当开始执行此函数时,就会创建一个Execution Context的内部对象,该对象定义了函数运行时的作用域环境(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开我猜测一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)。Execution Context对象有自己的作用域链,在Execution Context创建时初始化,会将函数创建时的作用域链对象[[scope]]中的全部内容按照在[[scope]]作用域链中的顺序复制到Execution Context的作用域链中。

此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做Activation Object(激活对象),这个激活对象又是干嘛的呢?这个激活对象保存了函数中的所有形参,实参,局部变量,this指针等函数执行时函数内部的数据情况,这个Activation Object是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。)。具体情况如图所示:

image

图 2

我们从左往右看,第一部分是函数执行时创建的Execution Context,它有自己的作用域链,第二部分是作用域链中的对象,索引为1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的,第三部分是作用域链中的对象的内容Activation Object和Global Object。

函数在运行过程中,没遇到一个变量,都会去Execution Context的作用域链中从上到下依次搜索,如果在第一个作用域链(假如是Activation Object)中找到了,那么就返回这个变量,如果没有找到,那么继续向下查找,直到找到为止,这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及javascript中各种看似怪异的、有趣的作用域问题的答案(你可以用这种方法来解释你以前碰到的所有作用域问题,当然,如果还是有疑问的话,非常希望你能贴出代码,我们一起讨论。)

一般情况下,一个函数的作用域链是不会在函数运行时被改变的,但有些运算符会临时改变作用域链,with和try catch的catch子句。看下面的例子:

 

 

function initUI(){

       with (document){     //avoid!

       var bd = body,

       links = getElementsByTagName("a"),

       i= 0,

       len = links.length;

       while(i < len){

       update(links[i++]);

       }

       getElementById("go-btn").onclick = function(){

       start();

       };

       bd.className = "active";

       }//eOf with

       }

 

当代码执行到with时,Execution Context的作用域链被临时改变了,一个新的可变对象被插入到作用域链的顶部,这个可变对象包含了with指定的对象的所有属性。如果此时在with中访问函数的局部变量,就会先把新插入的可变对象遍历一遍,然后才会去Activation Object中查找,直到找到为止,此时查找效率就会降低(这也是很多人说不要使用with的原因,我认为只要设法不影响性能就行了,毕竟访问with语句指定的对象的属性还是很快的,关于性能的问题大家如果想了解的话,可以关注我的下一篇博文《javascript数据访问性能》),如图:

image

CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,