我们首先回顾和学习一下几个概念。
1. 执行期上下文
当函数执行前,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
我们在之前学习函数预编译的时候提到这个执行期上下文,也就是我们当时创建的AO对象,当我们多次执行函数的时候会生成多个AO对象,但是每个AO执行之后都会被销毁。
2. 作用域:[[scope]]
每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了执行期上下文的集合(这个可以理解为个体AO存储的内容的集合)。
3. 作用域链
作用域[[scope]]中所存储的执行期上下文对象的集合(而这个集合我们现在暂时可以理解为多个AO的集合,但这么说不太准确,因为里面还有GO,具体我们下面讲),这个集合呈链式链接,我们把这种链式链接叫做作用域链。
这里我们要知道的是js中通过栈存取来管理执行期上下文,那么栈数据结构的特点是什么呢?
我们总结成八个字:先进后出,后进先出
栈只有一个出口:
用这个盒子类比,最先放的球在最下面,最后放的最上面,要想取出下面的就需要把上面的一个一个拿出来。
接下来我们找个例子来学习:
function a(){
function b(){
var b = 1;
}
var a = 2;
b();
}
var glob = 100;
a();
首先我们要知道在哪个函数里查找变量就去哪个函数的作用域链去找。
1.在a函数被定义的时候,a函数有个属性(函数是对象。所以可以有属性)叫[[scope]],它里面存着一条作用域链(scope chain),但此时,作用域链上只有一个GO对象
2.在a函数执行的时候,会在作用域链的顶端存入a的AO对象(栈操作),这个时候作用域链的最顶端就是我们a的AO了。那么函数变量初始化的值我们就可以在AO中去寻找了,AO中没有再向下在GO里面找
3.函数a的执行产生了b函数的定义,因为b函数定义在a的函数体里面。那么b函数定义的时候,也会有一个自己的scope属性,scope属性里面应该有一条作用域链,但这条作用域链和a的作用域链是不一样的。因为a函数被定义的时候,它是在全局环境下进行的,而b的定义它的环境是函数a里面。
a,b的作用域链类似于父子关系,父辈小时候发生的事情孩子小时候可以知道,但是孩子小时候会发生的事情父亲在自己小时候是可能知道的。这里a的作用域链是父亲,b的作用域链是孩子。
那也就是说b的作用域链的基础是a的作用域链,有点像孩子继承父亲财产的意思在里面,父亲最开始是白手起家,到了孩子那里,已经有了财富的积累。
如图:
4.那么b的执行和a的执行应该是一样的,会给b自己的作用域链上添加一个属于b的AO对象,并且由于栈的原理,b的AO就会在作用域链的最顶端。这个时候b里面的函数要查找指定变量的时候就会先查看b的AO对象,如果没有找到就顺着作用域链查看a的AO对象,还没有就继续往下找,直到GO也被查找完。
这里我们思考一个问题,a函数执行产生的属于a的AO对象和b函数被定义时得到的a的AO对象是同一个吗
function a(){
function b(){
var b = 1;
a = 3;
}
var a = 2;
b();
console.log(a);
//3
}
var glob = 100;
a();
这里我们通过在函数b里改变变量a的值来观察函数a的变量a是否发生变化,因为如果是同一个对象,变量a的值必然会发生变化。
这里我们发现变量a 的值发生了变化,由最初定义的2变成了3,那么就证明a函数执行产生的属于a的AO对象和b函数被定义时得到的a的AO对象是同一个。b函数在定义的时候获取到了函数a的AO对象的引用。
5.那么我们之前也说到函数执行完之后,会销毁自己的执行期上下文。a函数执行完的标志是b函数执行完,因为b函数执行是a函数的最后一条语句。b函数执行之后会把自己的执行期上下文给销毁。
那么a函数执行完也是一样的,销毁掉自己的执行期上下文:
这个时候a销毁的AO对象里有着b函数
这也就是说a函数执行完之后,b函数直接没有了。然后a函数变成最初定义的时候,等到a函数执行又会形成一个新的AO,而这个AO里面依然有b函数。同理,b函数在a函数执行时候被定义,获取a的AO和一个GO,然后自己执行获取自己的AO,相当于重复了上面的操作,然后执行完又是相同的销毁操作。
当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。
内存泄露不是指内存流失,而是说闭包它产生的一个后果是导致原有的该释放的作用域链没有释放(具体我们下面讲),这样就会占用内存空间,变相的使得可用内存空间变小,这被称之为内存泄漏。
举个栗子:
function a(){
function b(){
var c = 1;
console.log(d);
//3
}
var d = 3;
return b;
}
var start = a();
start();
这个函数执行过程我们可以分为几步:
那么a的作用域链没有被释放是怎么回事呢?
函数a执行完销毁了自己的AO,(这里要注意关键地方:正常情况因为函数b是在函数a里面声明的,所以b函数是存在于a函数的AO对象中的,当最后函数a执行完,并且a销毁掉自己的AO对象时b函数会被销毁)但是此时函数b才开始执行,他先生成自己的AO,然后执行完销毁掉自己的AO,这个过程中函数b始终保存着函数a的AO对象。也就是说b函数保存进全局环境中的时候,一直拥有着GO和函数a的AO。这也就导致a的AO一直没有被销毁,因为它一直在被使用,不会被当做内存垃圾回收掉,因此导致函数a作用域链不释放。
那么闭包的作用有哪些?
如函数累加器:
function add(){
var num = 0;
function count(){
num ++;
console.log(num);
}
return count;
}
var result = add();
result();
//1
result();
//2
function eater(){
var food = "";
var obj = {
eat : function(){
console.log("i am eating " + food);
//i am eating egg
food = "";
},
push : function(foodName){
food = foodName;
}
}
return obj;
}
var demo = eater();
demo.push('egg');
demo.eat();