看一段代码:
function f(){ var n = 999; function f1(){ console.log(n+=1); } return f1; } var result = f(); result(); result(); result();
看了上面代码之后 你晕了吗?
下面我们开始讲闭包,因为代码会造成内存泄漏的问题,讲闭包之前,首先让我们了解一下js的执行环境,变量的执行环境有助于确定应该何时释放内存。随后我们会讲下内存回收机制,之后再来看这段代码。
1.执行环境
执行环境定义了变量或者函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
js在进入一段可执行代码时,需要完成以下3个初始化工作:
a)创建全局对象Global Object
这个对象全局只存在一次,且它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期。但是这个全局对象不能直接通过名字访问,另提供了windows对象作为其属性,并指向自身,来对外使用。
b)构建执行环境栈,与此同时创建一个全局执行环境EC,并将全局执行环境EC压入执行环境栈中。
执行环境栈的作用是为了保证程序能够按照正确的顺序被执行。在JavaScript中,每个函数都有自己的执行环境。当js执行一个函数时,首先会把该函数的执行环境推入执行环境栈的顶部并获取执行权,当这个函数执行完毕时,它的执行环境又从这个栈的顶部被删除,并把执行权交给之前的执行环境。
c)创建一个与EC关联的全局变量对象VO,并把VO指向全局对象。
VO中不仅包含了全局对象的原有属性,还包括在全局定义的变量X和函数A,与此同时在定义函数A的时候,还为A添加了一个内部属性scope,并将scope指向了VO。每个函数在定义的时候,都会创建一个与之关联的scope属性,scope总是指向定义函数时所在的环境。
下面我们对整个流程进行总结:
step1: 开始执行js前
创建执行环境栈
step2:执行函数A
创建函数A的执行环境EC,并把EC推入执行环境栈的顶部获取执行权。然后创建函数A的作用域链。接着创建一个当前函数的活动对象AO。然后把AO推入作用域链的顶端。
每个执行环境都有自己的作用域链,初始化为当前运行函数A的scope所包含的对象。作用域链的作用是保证执行环境有权访问的所有变量和函数的有序访问。AO中包含了函数的形参、arguments对象、this对象以及局部变量和内部函数的定义。
2.JavaScript垃圾回收机制
变量不再使用时即被回收:定义该变量的函数调用结束时,该变量就会被回收销毁。
讲我们了以上两个准备知识点,下面我们开始看闭包的代码。
function f(){ // line1 var n = 999; // line2 function f1(){ // line3 console.log(n+=1); // line4 } // line5 return f1; // line6 } // line7 var result = f(); // line8 result(); // line9 result(); // line10 result(); // line11
结果如下:
依次打印1000 1001 1002
执行代码line8时,进入函数f的执行,首先把函数f压入执行环境栈,函数f内定义的函数及变量压入函数f的作用域链(按定义顺序压入,而变量的访问是要追溯作用域的,所以后定义的会覆盖阡陌定义的同名变量或者函数)。
执行到line6时,返回的是f1函数的引用,指针指向函数f1所在的内存地址。
执行完lin8时,由于line9调用了函数f1(line8的result指向函数f1所在的内存地址),函数f1的执行环境还在执行环境栈,没有被推出。由于f1函数内引用了定义f1所在环境的变量n,所以不能释放函数f的执行环境.所以变量n会一直在内存中。
以上解释了闭包能保存变量以及能造成内存泄漏的原因。
下面来让我们再看一段代码:
for (var i = 0; i < 4; i++) { (function (i) { setTimeout(function () { console.log(i) }, 0) })(i) }
结果是:0 1 2 3
这段代码也是闭包的使用.匿名函数可以用来创建闭包也能用来减少命名空间的污染
闭包延长了定义闭包的函数的生命周期。