作为第一次接触闭包,就将自己的理解及查询相关信息进行总结,在谈论闭包之前,先了解几个相关知识:执行上下文、作用域链、垃圾回收机制;
先从执行上下文开始,什么是执行上下文呢,每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。执行上下文(简称-EC)是一个抽象概念,ECMA-262标准用这个概念同可执行代码(executable code)概念进行区分。
Javascript中代码的运行环境分为以下三种:
-
全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
-
函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
-
Eval的代码 – 在Eval函数内运行的代码。
如:
var testvar = "hello world!";
function test(){
var name = "Tom";
function Rname(){
return name;
}
}
其中,所有代码属于全局级别代码,也就是全局性上下文,而test函数中则是函数级别的代码,即函数的上下文。注意的是,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部作用域,可以在该局部作用域中声明私有变量等,在外部的上下文中是无法直接访问到该局部作用域内的元素的。
当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。
由此可见 ,对于执行上下文这个抽象的概念,可以归纳为以下几点:
-
单线程
-
同步执行
-
唯一的一个全局上下文
-
函数的执行上下文的个数没有限制
-
每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
执行上下文的建立过程
我们现在已经知道,每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:
-
建立阶段(发生在当调用一个函数时,但是在执行函数体内的具体代码以前)
-
建立变量,函数,arguments对象,参数
-
建立作用域链
-
确定this的值
-
-
代码执行阶段:
-
变量赋值,函数引用,执行其它代码
-
实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:
1
2
3
4
5
executionContextObj = {
variableObject : { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ } ,
scopeChain : { /* variableObject 以及所有父执行上下文中的variableObject */ } ,
this : { }
}建立阶段以及代码执行阶段的详细分析可查看参考博文源地址: http://www.360weboy.com/frontdev/javascript/execution-context.html
而在建立上下文中,有建立作用域链,那么什么又是作用域链呢?
我们都知道,在JavaScript中,变量的作用域有全局作用域和局部作用域两种。在JavaScript中,函数也是对象,函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。例如定义下面这样一个函数:
1234function add(num1,num2) {var sum = num1 + num2;return sum;}在函数add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量,如下图所示:
-
函数add的作用域将会在执行时用到。例如执行如下代码:
1
|
var total = add(5,10);
|
执行此函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。每个运行期上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。
这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。新的作用域链如下图所示:
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。
博文源地址:http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
还有就是垃圾回收机制(GC:GarbageCollecation),GC在回收内存时,首先会判断该对象是否被其它对象引用。在确定没有其它对象引用便释放该对象内存区域。因此如何确定对象不再被引用是GC的关键所在。
.<script>
functionaa(){this.rr=“弹窗”;}
functionbb(){this.rr=“弹窗”;}
functioncc(){var a1=newaa();var b1=newbb();returnb1;}
var b1=cc();
alert(b1.rr);
</script>
此时cc函数中的a1,b1都是局部变量,但仍然会弹出文字窗口。说明b1并没有被GC回收。因此JavaScript中局部变量不是所有时候都被GC回收的。
而对于闭包(closure),Javascript闭包(closure)深入浅出,以函数cc为例变量层级关系为:
window<=>cc<=>a1<=>rr2.<=>b1<=>rr
在执行cc()方法时,文字解释如下:
.window的活动对象包括cc,假设window是顶级对象(因为运行中不会被回收)
·cc的活动对象包括a1和b1,其作用域链是window·a1的活动对象包括rr,其作用域链是cc
·b1的活动对象包括rr,其作用域链是cc
执行cc()时,cc的执行环境会创建一个活动对象和一个作用域链。其局部变量a1,b1都会挂在cc的活动对象中。当cc()执行完毕后,执行环境会尝试回收活动对象占用的内存。但因局部变量b1通过returnb1,为其增加了一条作用域链:window<=>b1<=>rr,所以GC停止对b1回收。
我们都知道,Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量,在函数外部自然无法读取函数内的局部变量。那么,我们有时候需要得到函数内的局部变量,如何能实现呢,就是闭包,我的理解呢,闭包就是能够读取其他函数内部变量的函数;如下:
function f1(){
function f2(){
var temp = 100;
document.write(temp)
}
return f2;
}
var result = f1();
result();// 100;
其中,函数f1中的局部变量temp一直保存在内存中,并没有在f1调用后被自动清除,为什么呢,原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,也就是被result所引用,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。这也就是闭包的两个用途:一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。