【JS】闭包专题

361 阅读6分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

一、初识闭包

还记得初识闭包时,在书上看到闭包概念是这样的:

《JavaScript高级程序设计(第3版)》:闭包,是指有权访问另一个函数作用域中的变量的函数。

《JavaScript高级程序设计(第4版)》:闭包,指的是那些引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现的。

从上述书本定义来看,我们需要先了解作用域,才能真正理解啥是闭包。那么接下来我们先学习下作用域,再来揭开”闭包”的神秘面纱吧~~

二、相关概念

作用域&作用域链

1、作用域

概念

作用域,简单来讲就是变量的可用范围。

为什么需要作用域?

作用域,是用来防止不同范围的变量之间互相干扰。

作用域类型

1)全局作用域

不属于任何函数的外部范围称为全局作用域

全局变量,保存在全局作用域的变量。
优点: 可反复使用
缺点: 全局污染

image.png

2)函数作用域

一个函数内的范围称为函数作用域

局部变量,保存在函数作用域内的变量。
优点: 不会被污染
缺点: 无法反复使用 image.png

2、作用域链

js规定,一个函数,既能用自己作用域的变量,又能使用外层作用域的变量。当查找一个变量时,当前函数先寻找,未找到继续在上层作用域查找,由内向外,直到全局作用域,因而形成链条,就叫作用域链image.png 作用域链,就像生活中我们规划景点“游览线路”一样,由近及远。 image.png

作用域的本质

JS中,作用域和作用域链都是对象结构。我们先来看个demo案例,debug断点来探究下作用域的本质: image.png 由上图可知:

  • 函数作用域其实是一个js引擎临时创建的对象,只不过没有对象名。
  • 函数作用域对象中保存着函数内所有局部变量。

继续调试代码,当执行调用完fun()函数,观察作用域的变化,如下图: image.png

总结:

  1. 函数作用域,其实是js引擎在调用函数时才临时创建的一个作用域对象(也叫活动的对象AO),其中保存函数的局部变量。而函数调用完,函数作用域就释放了。
  2. 全局作用域是Global作用域对象(GO)。

函数调用过程 VS 做饭三步曲

第1步:准备食材

image.png

第2步:做菜

image.png

第3步:收拾厨房

image.png

三、再探闭包

从用法上理解闭包

给一个函数,保存一个既可反复使用,又不会被外界污染的专属局部变量,该函数就是闭包

使用闭包“三步曲”

第1步:

外层函数包裹要保护的变量和使用变量的内层函数

第2步:

在外层函数内部返回内层函数对象

第3步:

调用外层函数,用变量接住返回的内层函数对象。

// 第1步:用外层函数包裹要保护的变量和使用变量的内层函数
function mother() {
  var total = 1000;
  // 第2步:返回内层函数对象
  return function(money) {
    total -= money;
    console.log(`花了${money}还剩${total}元`)
  }
}

// 第3步:调用外层函数,用变量接住内层函数对象
var pay = mother();
// pay接住的是mother返回的内层函数
pay(100);// 剩900
total = 0;
pay(100);// 剩800

闭包的产生原因

闭包的出现,是为了实现既重用变量保护变量不被污染的需求。

闭包的本质

闭包也是一个对象,即每次调用外层函数,临时创建的函数作用域对象。

一句话概括:闭包如何形成的?
外层函数调用时,外层函数的作用域对象被内层函数引用着,无法释放,就形成了闭包对象。

闭包结构 VS 妈妈发小宝宝红包

闭包结构,我们可以结合生活中妈妈生小宝宝的场景类比理解:

  • 外层函数——怀了小宝宝的妈妈
  • 内层函数(多个)——小宝宝(们)
  • 外层函数的局部变量——妈妈给宝宝的专属红包
  • 调用外层函数妈妈,生了小宝宝,小宝宝拿着属于自己的红包,自立门户! image.png

闭包的优缺点

优点

使用闭包的优点是可以避免全局变量污染。

缺点

闭包中,由于内层函数一直引用着外层函数的作用域对象,不能及时释放,极容易造成内存泄露。

解决闭包造成的内存泄露

及时释放不用的闭包:
将保存内层函数对象的变量赋值为null,内层函数引用的闭包对象没人要了,闭包对象就被释放回收了。

function mother() {
  var total = 1000;
  // 第2步:返回内层函数对象
  return function pay(money) {
    total -= money;
    console.log(`花了${money}还剩${total}元`)
  }
}

var pay = mother();

// 不用时,及时释放闭包
pay = null;

四、闭包的笔试/面试题

考察闭包知识点,主要会出如下两类题型:

类型一:看程序,判断输出

1)顺产1个、剖腹产1个孩子(公用红包)

function fun(){//妈妈
  var i=999;//红包 
  //给从未声明过的变量赋值,自动在全局创建该变量 
  //剖腹产
  nAdd=function(){i++};//孩子1 
  //妈妈顺产生了一个孩子2
  return function(){//孩子2
    console.log(i) 
  }
}
var getN=fun(); //妈妈包了一个红包,生了2个孩子 //结果: 两个孩子共用同一个红包! 
getN();//红包=999 
nAdd();//(红包+=1)红包=1000 
getN();//红包=999

// 输出结果:999、1000

2)生了2次孩子(独享红包,互不影响)

function mother() {//妈妈
  var i = 0; //红包
  return function () {//孩子
    i++;
    console.log(i);
  };
}
var get1 = mother(); //妈妈生老大,给老大包一个红包 
get1();//老大的红包是1
var get2 = mother(); //妈妈又生了老二,又给老二包一个新的红包 
get2();//老二的红包是1
get1(); //老大的红包+1,变成2
get2(); //老二的红包+1,变成2

// 输出结果:1、1、2、2

3)剖腹产3个孩子(公用红包)

function fun(){//妈妈 
  arr=[];//不算红包,因为内层函数中没有用到! 
  //但是arr是给未声明的变量强行赋值 
  //自动在全局创建该变量
  for(var i=0/*红包*/;i<3;i++){//循环三次 //强行给外部全局变量中添加新函数——破腹产 
    // new Function() 
    arr[i]=function(){//反复创建了3个孩子
      console.log(i);//因为只是创建函数,所以这里暂时不执行。 
    }
  }
  //for结束后,i=3 
}
fun(); //妈妈一次刨妇产生了3个孩子,包了一个共用的红包,红包里是3块钱 
arr[0]();//3
arr[1]();//3
arr[2]();//3

// 输出结果:3、3、3

类型二:根据需求,用闭包实现功能

修改下面的代码,让循环输出的结果依次为0, 1, 2, 3, 4:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}
// 输出结果:55555

1)使用闭包

for (var i = 0; i < 5; i++) {
  log(i);
}
function log(i) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

2)立即执行函数

for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

3)es6块级作用域

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

参考:zhuanlan.zhihu.com/p/25407758