这是大虾的第一篇博文,大虾试图用最直白的语言去描述出所理解的东西,大虾是菜鸟,水平有限,有误的地方但愿路过的朋友们务必指正,谢谢你们了。javascript
从读书时代一路走来,大虾在学习的时候逐渐喜欢上了去追寻根源,这个东西究竟是为何?他有什么用处?他解决了什么问题?他是怎么被想到的?从这些问题当中,咱们可以学到很是多,大虾深有体会。我相信,即便是这些东西在发明之时,就算是创始人也未必思考的这么周全,不少状况下,它必定是先遇到了什么实际问题以后,再去思考解决方案。也就是说,每个新知识新东西的提出,必定是为着解决某个问题而出现的,不然他的存在就没有意义,而脱离了实际问题的学习也是没有意义的。在学习这个东西的过程当中,大虾很看重的是,假如一样的是遇到了这个问题,大虾会怎么作去解决?别人又是怎么解决的?为何别人的解决方案这么优秀?他是怎么想到的?我怎么就想不到呢?这种过程使大虾受益不浅。前端
话很少说,切入正题。本文主要介绍闭包。相信不少人只知道学闭包,直接去看他的原理,实现过程什么的,可是根据个人了解来看,初学者知道闭包的用途的并很少,为何须要闭包?这些都不清楚,然而在大虾看来,这个很是重要,由于它对应着实际问题的解决,理论脱离了实践将变得毫无心义。java
话说二十年前,祖师爷创立js的时候,那时候页面并不复杂,大型的网页也很少,页面没什么js的时候,人们写页面时全局变量是随便定义的,直到某一天,随着页面js的增多,问题来了。拿一个模拟的alert来讲,若是这么写:闭包
var temp=a; var abs=function(){}; var yourAlert=function(){};
那么在这种状况下,全局变量temp,abs,yourAlert就被污染了。也就是说,若是要实现另一个功能,好比说按钮btn,这个时候也须要写本身的代码,那么它的变量起名必须从新起名,必须避开上面的变量,不然就会把上面的内容给覆盖掉。这些新功能一旦数量不少,那么你的起名就必须避开全部的已用过的变量名,你必须挨个检查全部功能的变量名以保证他的不重复,这样就给开发带来很大的不方便。因此迫切的须要一种方法来避免它,来保护变量不被篡改污染。再者,在页面中,常常遇到屡次调用的状况,一样以上面的alert为例,假如用户触发了10次alert,若是说每一次的触发都要从新建立一个alert的话,那样岂不是特别麻烦,特别消耗内存?这个时候一样须要一种可以反复调用的方法来优化代码的执行性能。 函数
闭包应运而生。性能
来看看闭包的实现过程,他究竟是如何实现而且达到上述目的并解决实际问题的呢?深入的理解整个本质的实现过程有助于咱们的开发运用。学习
当一个函数被调用时,一个执行环境(也称执行上下文)就会被建立(execution context),然而在js引擎内部,这个执行环境建立过程被分为了2个阶段:优化
一、 创建阶段(此时还并无执行具体的函数体的代码)this
创建变量对象(variable object):函数里面的arguments对象、函数参数、内部变量、函数声明spa
一、 创建arguments对象,检查当前环境下的参数,创建该对象下的属性和属性值
二、 检查当前环境下的函数声明:每找到一个函数声明,就会在变量对象里用函数名创建一个属性,属性值就是指向函数地址的引用。若是该函数名已经存在,那么其对应的属性值就会指向新的引用。
三、 检查当前环境下的变量声明:每找到一个变量声明,就在变量对象下创建一个属性,其值为undefined(此时还未赋值)。若是该变量名已经存在,会直接跳过(防止指向函数的属性值被变量属性覆盖为undefined)
创建做用域链
当前变量对象被添加到执行环境的前端
肯定this的值
二、 代码执行阶段
执行函数中的代码,对变量赋值、函数引用、执行其余代码等等…
具体的来看一个函数的代码:
1 function f(x){ 2 var a=20; 3 var b=function(){ 4 5 }; 6 function d(){ 7 8 }; 9 }
f(100);
在调用f(100)的时候,执行环境的创建阶段发生以下变化:
fExecutionContext{ // f函数执行环境
variableObject:{// f函数变量对象(对于函数来讲,也称为活动对象AO)
arguments:{
0:100;
length:1;
},
x:100,
d:pointer to function d()//指向d函数的引用,实际上保存的是地址,它的顺序也在变量声明之上
a:undefined,
b:undefined,
},
scopeChain:{....},//做用域链
this:{...}// this值
}
当上述创建阶段结束,js引擎立马进入执行阶段,一行一行的运行函数代码,给variableObject的属性赋值,执行阶段完成以下:
fExecutionContext{ // f函数执行环境 variableObject:{// f函数变量对象(对于函数来讲,也称为活动对象AO) arguments:{ 0:100; length:1; }, x:100, d:pointer to function d()//指向d函数的引用,实际上保存的是地址,它(函数声明)的顺序也在变量声明之上 a:20, b:pointer to function b(), }, scopeChain:{....},//做用域链 this:{...}// this值 }
事实上,若是这个环境是函数,变量对象并不可以被直接访问到,此时函数的活动对象(AO)将代替变量对象的角色。咱们能够看到,上述环境中,执行环境的做用域链也被建立完成。
那做用域链又是什么呢?事实上,在函数的建立时,会预先建立一个包含全局变量对象的做用域链。做用域链的本质是一个指向变量对象的指针列表!它只是引用而实际上并不包含变量对象!为了方便理解,能够把它想象成一根链条(实际上并非真的存在这么一根链条),上面依次标记着顺序0,1,2,3.......,每个数字对应着一个变量对象。在访问变量对象中的属性时,只能按照标记的顺序按0,1,2,3...的顺序依次访问,在这个链条的最前端,始终是当前执行的代码所在的环境的变量对象(能够理解为顺序标记为0),建立执行环境时,当前函数的活动对象将会被推入到做用域链的最前端!而下一个变量对象则来自其外部函数(顺序标记为1),再下一个则是外部函数的外部函数,全局执行环境的变量对象始终是链条的最后位置,全局的变量对象始终是最后一个被访问到。
在本例中,以函数f为例,其做用域链关系以下图
在寻找变量名和函数名的时候,会首先在顺序为0的位置的变量对象中寻找(也就是它本身的活动对象),若是找不到,就会向下一级变量对象寻找(函数的外部函数的变量对象,在做用域链中顺序标记为1),一直搜索到最后一个对象——全局环境的变量对象为止。全局环境的变量对象始终存在于每个做用域链中!当函数执行完毕后,函数的活动对象就会被销毁,内存中仅保存全局变量对象。
然!而!且!慢!闭包的状况不同啊!
看下面这个超级简单的例子。
function f(){ var a=20; return function d(){ a--; }; }
var sB=f(); sB();
上面的d函数是直接定义在函数f的内部中的,便是说函数f是函数d的外部函数,按照上面所述原则,在函数d的做用域链中,函数f的变量对象会被添加进函数d的做用域链中!此时函数d的做用域链一共有3个对象:函数d的活动对象会被标记为0,函数f的变量对象会被标记为1,全局变量对象标记为2,访问时按照0,1,2的顺序访问,寻找变量时先在本身的活动对象里找,再去顺序为2即函数f里面找,这样就能访问外部函数f中的全部变量。然而当函数f执行完毕时,它的执行环境的做用域链会被销毁,但它的活动对象并无被销毁,仍然保存在内存中,匿名函数d的做用域链仍然在引用着这个活动对象,直到匿名函数d被销毁,函数f的活动对象才会被销毁。这就是闭包的最大的不一样之处!
咱们来画个流程图,理解下闭包过程当中所发生的变化。从函数的建立开始。
前方高能,多图预警!请在wifi下点开,土豪请无视。
一、函数f的建立
二、函数f开始执行了
三、执行到 return语句
四、执行var sB=f();
五、调用函数d
六、执行完毕
这就是闭包的整个实现过程,闭包实现后,能够在全局反复调用内部函数d(),此时在即便全局定义相同的变量a,调用函数时,使用的值仍然是函数f的活动对象里面的值,外面的更改没法影响到局部变量a。这里只是作个简单的介绍,闭包还有不少应用状况,实际状况也更加复杂,还有很长的路要走。
参考书籍:《JavaScript高级程序设计第3版》
参考内容: http://tieba.baidu.com/p/2348703848
http://blogread.cn/it/article/6178?f=sa