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

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

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

复习,笔记,更深刻的理解。java

欢迎拍砖指正。express

做用域:

下面咱们先搞明白这样几个概念:缓存

  • 函数对象的[[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

图3

当try catch语句中try语句块中的代码发生错误时,会自动跳入catch语句块,而且会把catch语句指定的异常对象插入到做用域链的顶端,但catch有个特色,就是catch子句执行完毕以后,做用域链都会返回到原来的状态。

闭包:

  • A " closure" is an expression (typically a function) that can have free varuables together with an environment that binds those variables (that "closes" the expression). —— ECMA262
  • “闭包”是一个表达式(通常是函数),它具备自由变量以及绑定这些变量的环境(该环境“封闭了”这个表达式)。—— 李松峰
  • 闭包就是可以读取其余函数内部变量的函数。——阮一峰

对于闭包这个经典的话题,网上的前辈高手已经作过不少详尽的解释,若是我再过多的说明,显得有些班门弄斧,不过,对于闭包,理解的角度不一样,看到的面可能就不同。

这里咱们从做用域的角度来分析一下闭包产生的方式和特色。

咱们都知道,闭包容许咱们访问闭包函数做用域以外的做用域内的数据(说简单点就是能够闭包容许咱们访问闭包函数以外的函数的数据。),这是闭包的一个很是强大的功能,不少复杂的网页应用都和这个特性有关,例如:建立封闭的命名空间、保留外部函数执行环境。

咱们一块儿来看一个闭包的例子:

	function assignEvents(){
	var id = "xdi9592";
	document.getElementById("save-btn").onclick = function(event){
	saveDocument(id);
	};
	}

上例中,在onclick事件的事件处理器中引用了外部函数assignEvents的局部变量id,造成了闭包,下面咱们看一下它们的做用域图示:

image图 4

咱们一块儿来从做用域的角度分析一下闭包的造成过程:

image图 5

这也就是闭包为什么能“记得”在它周围到底发生了什么,为什么闭包能访问外层函数的局部数据,为什么闭包能保持这些局部数据而不在外层函数执行完毕销毁时一块儿销毁等等的缘由。

前些天一个前辈(Darrel文叔)告诉我一句话,一针见血:没有内存,就没有闭包。

性能问题:

在做用域链和闭包中的性能问题主要表如今数据读写的速度上。

因为做用域链的缘由,咱们访问全局做用域的数据(这里为何不说变量呢?由于不只包括变量,还有函数,对象等其余内容)时,效率是最低的,而访问局部数据时的效率是最高的。

因此一个很是经典的解决数据访问性能问题的方案出现了:将须要访问的数据尽可能的以局部数据的方式缓存起来。这样当标识符解析程序在做用域链中寻找数据时,直接就能够在做用域链的最上层找到想要的数据,效率天然就提高了。

这句话能够解决不少性能问题:设置缓存,将数据保存在局部变量中。

转载请注明出处:

参考:

  1. assignEvents函数建立,词法解析后,函数对象assignEvents的[[scope]]属性被初始化,做用域链造成,做用域链中包含了全局对象的全部属性和方法(注意,此时由于assignEvents函数并未执行,因此闭包函数并无被解析)。
  2. assignEvents函数执行,在开始执行时,建立Execution Context(咱们将图4按照从左到右,从上到下的顺序划分为6部分,第一部分就是运行期上下文),在运行期上下文的做用域链中建立Activation Object(第2、三部分),并将Activation Object放置与做用域链顶点,在其中保存了函数执行时全部可访问函数内部的数据。
  3. 当执行到闭包时,javascript引擎发现了闭包函数的存在,按照一般的手法,将闭包函数解析,为闭包函数对象建立[[scope]]属性,初始化做用域链(此时闭包函数对象的做用域链中有两个对象,一个是assignEvents函数执行时的Activation Object,还有一个是全局对象,图4的四、五、6部分。)。咱们看到图中闭包函数对象的做用域链和assignEvents函数的执行上下文做用域链相同?为何相同呢?咱们来分析一下,闭包函数是在assignEvents函数执行的过程当中被发现而且解析的,而函数执行时的做用域是Activation Object,那么结果就很明显了,闭包函数被解析的时候它的做用域正是assignEvents做用域链中的第一个做用域对象Activation Object,固然,因为做用域链的关系,全局对象做用域也被引入到闭包函数的做用域链中。     那么咱们如今考虑另外一个问题,闭包做用域链中的Activation Object,是引用了assignEvents函数的Activation Object,仍是拷贝了一个副本到闭包的做用域链中了?咱们能够作一个小的测试,在有多个闭包同时引用外层函数局部变量(i)的状况下,若是其中一个闭包改变了i的内容,而其余闭包中的i的内容没有发生改变,则说明产生了拷贝,反之,则引用了同一个Activation Object。

 

 

4. function fn(){

                     var i = 0;

                     (function(){++i;console.log(i)})();

                     (function(){++i;console.log(i)})();

                     }

                      

                     fn();

                     //1

                     //2

  1. 咱们发现变量i从1变为了2,说明两个闭包引用的是同一个变量i,也就说明他们引用的fn的Activation Object是同一个,其实彻底能够换一种很是简单的方式来解释:全局对象确定是同一个吧?
  2. 下面讨论当闭包函数执行时的状况,由于在词法分析的时候闭包函数就已经在做用域链中保存了对assignEvents函数的Activation Object的引用,因此当assignEvents函数执行完毕以后,闭包函数虽然尚未开始执行,但依然能够访问assignEvents的局部数据(并非由于闭包函数要访问assignEvents的局部变量id,因此当assignEvents函数执行完毕以后依然保持了对局部变量id的引用。而是不论是否存在变量引用,都会保存对assignEvents的Activation Object做用域对象的引用。由于在词法分析时,闭包函数没有执行,函数内部根本就不知道是否要对assignEvents的局部变量进行访问和操做,因此只能先把assignEvents的Activation Object做用域对象保存起来,当闭包函数执行时,若是须要访问assignEvents的局部变量,那么再去做用域链中搜索)。  
  3. 闭包函数执行时建立了本身的Execution Context和Activation Object,在运行期上下文的做用域链中保存了本身的Activation Object,外层函数assignEvents的Execution Context的Activation Object,以及Global Object,如图:
相关文章
相关标签/搜索