在js中,闭包主要涉及js中其它几个特性:作用域链,垃圾(内存)回收机制,函数嵌套等;
一、作用域链(Scope Chain)
在理解闭包前,最好要先理解作用域链。简单来说,作用域链就是函数在定义时创建的,用于寻找使用到的变量值的一个索引。而他内部的规则是将把函数自身的本地变量放在最前面,把自身的父级函数放在其次,再把高一级的函数放在后面,以此类推把全局变量放在最后面。
当函数寻找一个变量时,js解释器会先去作用域链中去查找,一层一层查找,直至到全局变量,直到找到这个变量为止,返回这个变量,否则返回undefined。js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。
每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
二、内存回收机制
一般来说,函数在执行开始的时候,会把其中定义的变量保存到内存中,以便后面的语句调用。等到函数执行完了,这些变量就被认为是无用的,就会将其对应的内存空间回收。等到下次再执行此函数的时候,所有变量又回到最初的状态,重新赋值使用。
但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的,并且这个函数又调用了外部的变量的话,这种内存回收机制就会出现问题
function outer(){
var scope = "outer";
function inner(){
return scope;
}
return inner;
}
var fn = outer();
fn();
outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象)
在外部函数返回后,又调用了内部函数,那么内部函数就无法读取到他所需要的外部函数变量的值了,所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(本地变量和父级函数和祖先级函数的变量)一起保存起来,也就是构建一个闭包。
三、闭包
闭包简单来说就是能读懂其他函数内部变量的函数。
闭包有两个作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中
关于第二个:
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
result[i] = function(){
return i;
}
}
return result;//返回一个函数对象数组
//这个时候会初始化result.length个关于内部函数的作用域链
}
var fn = outer();
console.log(fn[0]());//result:2
console.log(fn[1]());//result:2
由于result函数并未定义i变量,遂沿着作用域链查找,而这个变量i是父函数执行结束后将最终值保存在内存里的结果。 所以结果为2。由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。解决办法使用自执行函数
function outer(){
var result = new Array();
for(var i = 0; i < 2; i++){
result[i] = function(){
return i;
}();//预先执行函数写法
}
return result;
}
还可以使用ES6 let使函数变成块级作用域
关于闭包还有this指向问题,this对象是在运行时基于函数的执行环境绑定的。
在全局函数中,this指向window,当函数被当做某个对象调用时,this等于这个对象。而匿名函数具有全局性,因此this对象通常指向window
-----------------------------------------------------------------------------------
在网上看到了一个比较有意思的闭包题和大家分享一下:
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3);//undefined,0,1,2
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,0,1,1
//问:三行a,b,c的输出分别是什么?
1、第一行a
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
可以得知,第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以:
第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数。
遂:
在第一次调用fun(0)时,o为undefined;
第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;
第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2,0);所以o为0
第四次同理;
即:最终答案为undefined,0,0,0
2、第二行b
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
先从fun(0)开始看,肯定是调用的第一层fun函数;而他的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数。
遂:
在第一次调用第一层fun(0)时,o为undefined;
第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;
第三次调用 .fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时时(1,0)所以n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1;
第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2;
即最终答案:undefined,0,1,2
3、第三行c
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
根据前面两个例子,可以得知:
fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,遂c存放的是fun(1)的返回值,而不是fun(0)的返回值,所以c中闭包的也是fun(1)第二次执行的n的值。c.fun(2)执行的是fun(1)返回的第二层fun函数,c.fun(3)执行的也是fun(1)返回的第二层fun函数。
遂:
在第一次调用第一层fun(0)时,o为undefined;
第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;
第三次调用 .fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;
第四次.fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1