luochaotj 2012-06-24
这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存,关系到闭包,关系到javascript运行机制。关系到功能,关系到性能。
文章内容主要参考自《High Performance JavaScript》,这本书对javascript性能方面确实讲的比较深入,大家有空都可以尝试着阅读一下,我这里有中英电子版,需要的话QQ317665171或者QQ邮箱联系。
复习,笔记,更深入的理解。
欢迎拍砖指正。
下面我们先搞明白这样几个概念:
[[scope]]属性:
javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。
该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。例如(例子引用自《High Performance JavaScript高性能javascript》):
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
图 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就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。)。具体情况如图所示:
我们从左往右看,第一部分是函数执行时创建的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数据访问性能》),如图:
图3
当try catch语句中try语句块中的代码发生错误时,会自动跳入catch语句块,并且会把catch语句指定的异常对象插入到作用域链的顶端,但catch有个特点,就是catch子句执行完毕之后,作用域链都会返回到原来的状态。
对于闭包这个经典的话题,网上的前辈高手已经做过很多详尽的解释,如果我再过多的说明,显得有些班门弄斧,不过,对于闭包,理解的角度不同,看到的面可能就不一样。
这里我们从作用域的角度来分析一下闭包产生的方式和特点。
我们都知道,闭包允许我们访问闭包函数作用域之外的作用域内的数据(说简单点就是可以闭包允许我们访问闭包函数之外的函数的数据。),这是闭包的一个非常强大的功能,很多复杂的网页应用都和这个特性有关,例如:创建封闭的命名空间、保留外部函数执行环境。
我们一起来看一个闭包的例子:
function assignEvents(){
var id = "xdi9592";
document.getElementById("save-btn").onclick = function(event){
saveDocument(id);
};
}
上例中,在onclick事件的事件处理器中引用了外部函数assignEvents的局部变量id,形成了闭包,下面我们看一下它们的作用域图示:
我们一起来从作用域的角度分析一下闭包的形成过程:
这也就是闭包为何能“记得”在它周围到底发生了什么,为何闭包能访问外层函数的局部数据,为何闭包能保持这些局部数据而不在外层函数执行完毕销毁时一起销毁等等的原因。
前些天一个前辈(Darrel文叔)告诉我一句话,一针见血:没有内存,就没有闭包。
在作用域链和闭包中的性能问题主要表现在数据读写的速度上。
由于作用域链的原因,我们访问全局作用域的数据(这里为什么不说变量呢?因为不仅包括变量,还有函数,对象等其他内容)时,效率是最低的,而访问局部数据时的效率是最高的。
所以一个非常经典的解决数据访问性能问题的方案出现了:将需要访问的数据尽量的以局部数据的方式缓存起来。这样当标识符解析程序在作用域链中寻找数据时,直接就可以在作用域链的最上层找到想要的数据,效率自然就提升了。
这句话可以解决很多性能问题:设置缓存,将数据保存在局部变量中。
转载请注明出处:
参考:
4. function fn(){
var i = 0;
(function(){++i;console.log(i)})();
(function(){++i;console.log(i)})();
}
fn();
//1
//2