这多是每个jser都曾经为之头疼的却又很是经典的问题,关系到内存,关系到闭包,关系到javascript运行机制。关系到功能,关系到性能。javascript
文章内容主要参考自《High Performance JavaScript》,这本书对javascript性能方面确实讲的比较深刻,你们有空均可以尝试着阅读一下,我这里有中英电子版,须要的话QQ317665171或者QQ邮箱联系。html
复习,笔记,更深刻的理解。java
欢迎拍砖指正。express
下面咱们先搞明白这样几个概念:缓存
[[scope]]属性:网络
javascript中每一个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每一个函数对象都具备的一个仅供javascript引擎内部使用的属性,该属性是一个集合(相似于链表结构),集合中保存了该函数在被建立时的做用域中的全部对象,而这个做用域集合造成的链表则被称为ScopeChain(做用域链)。闭包
该做用域链中保存的做用域对象,就是该函数能够访问的全部数据。例如(例子引用自《High Performance JavaScript高性能javascript》):函数
function add(num1, num2){性能
var sum = num1 + num2;学习
return sum;
}
当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
转自 红黑联盟 http://www.2cto.com/kf/201111/110023.html