大厂面试之闭包问题详解
1、闭包的表面现象
2、闭包的底层原理
3、闭包的形式
4、经典题
什么是闭包?
闭包的表面现象
产生闭包的条件:
1、有父子函数的关系
2、子函数使用了父函数的变量
3、子函数有调用
如下代码,子函数可以使用父函数身上的变量
function father() {
var n = 10;
function son() {
n++;
console.log(n)
}
son()
}
father()
下面这个也可以形成闭包
function father() {
var n = 10;
function son() {
n++;
console.log(n)
}
return son;
}
var result = father();
result();
将son这个函数 return出来,让result接收,result接收的就是function(){…}
闭包的底层原理
- 变量的生命周期
- 垃圾回收机制
- 执行上下文
- 上下文执行栈
1、变量的生命周期--定义一个变量到不在使用这个变量,局部变量和全局变量
-
局部变量的生命周期在函数执行完成以后就到头了
-
全局变量的生命周期在页面关闭后就到头了
当fn()调用完成后,a的生命周期就到头了
function fn(){
var a = 10;
}
fn();
console.log(a);
大家都知道console里面的a是访问不到的,这是为什么呢?因为js有垃圾回收机制
2、垃圾回收机制
-
标记清除(大部分浏览器都使用的这种技术)–会给每一个被声明的变量打上标记(进入环境),如下,
在var a的时候会标记‘进入环境’,在fn()调用的时候会标记‘离开环境’
在‘离开环境’的时候垃圾回收机制就回收了这个变量
function fn(){var a = 10} fn() -
引用计数–在var a 的时候标记一个1,当代码中出现a = null,标记‘0’,然后就被回收
注意:如果这个数据有引用的关系,就不会被回收
3、执行上下文(当前代码的执行环境,简称EC)当js代码在执行过程中,遇到一下三种情况都会 产生一个EC,然后把EC放入ECS(上下文执行栈)中
-
全局环境
-
函数环境
-
eval环境–有安全性问题,不建议用
function father(){
function son(){
}
son()
}
father()
1、代码走到script标签的时候,此时是一个全局环境,然后会创建一个执行上下文,名字叫做全局环境执行上下文(globalEC),把创建的globalEC放入ECS。
2、接着往下走,走到函数调用father()的时候,会创建一个函数环境执行上下文(fatherEC),放入ECS。
3、接着走函数内部,又创建一个函数环境执行上下文(sonEC),放入ECS,当son()调用完了后,把sonEC从栈里拿出来删掉,代码执行完后,globalEC出栈
执行栈如下图所示------== 图一进栈,图二出栈 ==
再来一段代码解释上下文执行栈
function father(){
var n = 10;
function son(){
n++;
console.log(n)
}
return son;
}
var result = father();
result();
1、代码走到script标签的时候,此时是一个全局环境,然后会创建一个执行上下文,名字叫做全局环境执行上下文(globalEC),把创建的globalEC放入ECS。
2、接着往下走,走到函数调用father()的时候,会创建一个函数环境执行上下文(fatherEC),放入ECS,由于函数内部没有调用son,所以函数走完了fatherEC就出栈了,
3、接着走到result(),又创建一个函数环境执行上下文(sonEC),放入ECS,当son()调用完了后,把sonEC从栈里拿出来删掉,代码执行完后,globalEC出栈
4、上下文执行栈(ECS),别名,函数调用栈(call stack)
- 变量对象
创建完EC后需要走两个阶段
1、创建阶段
2、代码执行阶段
两个阶段的代码如下展示,展示的只是伪代码
function father(){
var n = 10;
function son(){
n++;
console.log(n)
}
return son;
}
var result = father();
result();
result();
//fatherEC的内部走的路子
// 1、创建阶段
fatherEC = {
VO:{ //变量对象
// 找变量、找函数、找参数、找arguments对象
n:undefined, //找变量声明
son:'son在内存里的引用地址'
},
scope:[ //存储作用域链
Global.AO,
],
this:'window'
}
// 2、执行阶段--伪代码
fatherEC = {
AO:{ //变量对象
n:10, //变量赋值
son:'...' //函数赋函数的引用
},
scope:[ //存储作用域链
fatherEC.AO,Global.AO,
],
this:'window'
}
//sonEC的内部走的路子
// 1、创建阶段----伪代码
sonEC = {
VO:{ //变量对象
},
scope:[ //存储作用域链
fatherEC.AO,Global.AO,
],
// this:'window'
}
// 2、执行阶段--伪代码
sonEC = {
AO:{ //变量对象
},
scope:[ //存储作用域链
sonEC.AO,fatherEC.AO,Global.AO,
],
// this:'window'
}
//因为son里面有n,所以n就会在sonEC中的执行阶段中的scope中从
//左往右的找n,sonEC.AO里面的VO没有,就找fatherEC.AO,直到找到n为止。
闭包的形式
下面的代码不是一个闭包,仔细分析会发现innerFn的参数
var a = 20;
function wrapFn(){
var b = 10;
function innerFn(b){
console.log(b);
}
return innerFn;
}
var fn = wrapFn();
fn(a);
下面的代码是一个闭包 --闭包的形式
var a = 20;
function wrapFn(){ // 这个函数就是一个闭包---根据谷歌浏览器为准
var b = 10;
function innerFn(){
console.log(b);
}
return innerFn;
}
var fn = wrapFn();
fn(a);
function wrapFn(){ //1
var a = 10;
return function(){ //2
var b = 20;
return function(){ //3
console.log(b) //此时的闭包函数是2
console.log(a) //此时的闭包函数是1 2
debugger;
}
}
}
var son = wrapFn()()();
有一个特殊情况的闭包—只要子函数与父函数能够凑到一起满足闭包的条件,那就形成了闭包
此时的innerFn1使用了父函数的变量,但是没有调用innerFn1,调用的是innerFn2
function wrapFn(){ //这是一个闭包函数
var a = 10;
function innerFn1(){
return a; //innerFn1是一个子函数,它的里面用到了a,这个a是父级的,但是这个函数并没有被调用
}
function innerFn2(){ //innerFn2是一个子函数,它里面没有用到a,但是它调用了,所以就让父函数形成了一个闭包环境
debugger;
}
innerFn2()
}
wrapFn();
经典题
<body>
<ul>
<li>red</li>
<li>pink</li>
<li>green</li>
<li>blue</li>
<li>yellow</li>
</ul>
<script>
var lis = document.querySelectorAll('li');
for(var i =0;i<lis.length;i++){
lis[i].onclick = function(){
console.log(i)
}
}//i每走一次,就会垃圾回收机制回收,改善后的代码为如下
var lis = document.querySelectorAll('li');
for(var i =0;i<lis.length;i++){
(function(){
lis[i].onclick = function(){
console.log(i)
}
}(i));//for循环走一次,声明的i在function里面有引用的关系,
// 有引用的关系就不会被回收,然后5个i就会被存在内存中
}
</script>
</body>
注意:
1、普通函数,定义函数的时候是嵌套的,调用的时候也是嵌套的
2、闭包函数,定义函数的时候是嵌套的,调用的时候是独立的