我从来不理解JavaScript闭包,但我用了它好多年

前言

  大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步!

  个人主页:南木元元

你是否学习了很久JavaScript但还没有搞懂闭包呢?今天就来聊一下被很多人誉为JavaScript中最难理解的概念之一的闭包。


目录

闭包的概念

闭包产生的原因

作用域&作用域链

闭包的本质

闭包的表现形式

闭包的用途

封装私有变量

做缓存

闭包的缺点

结语


闭包的概念

  • 红宝书(P309)上对于闭包的定义

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

  • MDN对闭包的定义

闭包是指那些能够访问自由变量的函数。其中自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

总结一下就是,闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

下面就是一个闭包的例子。

// 外部函数
function outerFunction() {
  let outerVariable = 'outer';
  // 内部函数
  function innerFunction() {
    console.log(outerVariable);
  }
  
  return innerFunction;
}
  
const innerFunc = outerFunction();
innerFunc(); // outer

在上面的代码示例中,函数outerFunction内部有一个innerFunction函数,innerFunction函数可以访问到outerFunction函数中的变量,此时函数innerFunction就是一个闭包。

闭包产生的原因

作用域&作用域链

首先需要知道作用域和作用域链的概念。

作用域就是变量与函数的可访问范围

在js中,有三种作用域:

  • 全局作用域:变量在整个全局中都能被访问到
  • 函数作用域:变量只能在当前函数内被访问到
  • 块级作用域:变量通过ES6中的let和const来声明,只能在⼀对花括号{ }包裹的块中访问

作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找,这种层级关系就是作用域链。

  • 静态作用域

js 采用的是静态作用域词法作用域),即函数的作用域在函数定义时就确定了

var num = 10;
function f1(){
  console.log(num)
}
function f2(){
  var num  = 20;
  f1()
}
f2();//10

以上代码的执行结果为10,这段代码经历了这样的执行过程:

  • f2函数调用,f1函数调用
  • 在f1函数作用域内查找是否有局部变量num
  • 发现没找到,于是根据书写位置,向上一层作用域(全局作用域)查找,输出10

静态作用域也称为词法作用域,即在词法分析时生成的作用域,词法分析阶段,也可以理解为代码书写阶段,当你把函数书写到某个位置,不用执行,它的作用域就已经确定了。与之相对的是动态作用域,函数的作⽤域在函数调⽤时才确定,如果采用动态作用域,那么上述结果为20(如果想深入了解,可以去看这篇文章)。

在了解了js的作用域和作用域链后,让我们来看看下面这段代码:

var num = 10;

function fn() {
  var num = 20;
  function fun() {
    console.log(num);//20
  }
  return fun;
}
var x = fn();
x();

上述例子中有三个作用域:全局作用域、fn的函数作用域、fun的函数作用域,它们的关系如下:

我从来不理解JavaScript闭包,但我用了它好多年_第1张图片

作用域链关系如下:

我从来不理解JavaScript闭包,但我用了它好多年_第2张图片

在这段代码中,fn的作用域指向有全局作用域和它本身,而fun的作用域指向全局作用域、fn和它本身。而作用域是从最底层向上找,当我们试图在fun这个函数里访问变量num的时候,此时函数作用域内没有num变量,当前作用域找不到,我们需要去上层作用域(fn函数作用域)找,在这里我们找到了num为20,输出即可(如果找到全局作用域还没有的话就会报错)。

闭包的本质

问大家一个问题:那是不是只有像上述例子一样返回函数才算是产生了闭包呢?

其实,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。因此我们还可以这么做:

var fun;
function fn() {
  var num = 2;
  fun = function() {
    console.log(num); //2
  }
}
fn();
fun();

让fn执行,给fun赋值后,等于说现在fun拥有了全局、fn和fun本身这几个作用域的访问权限,还是自底向上查找,最近是在fn中找到了num,因此输出2。

在这里是外面的变量fun存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。

闭包的表现形式

明白了本质后,那我们思考下,实际场景中,闭包是如何体现的呢?

  • 返回一个函数(上面已经举例)
  • 作为函数参数传递
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
// 输出2,而不是1
foo();
  • 定时器、事件监听或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

比如以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){
  console.log('111');
},100)

// 事件监听
$('#btn').click(function(){
  console.log('222');
})
  • IIFE(立即执行函数表达式)创建闭包,保存了全局作用域window和当前的函数作用域,因此可以使用全局的变量。
var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

现在,你是否会感叹一句:好家伙,原来我用了闭包这么多年!

闭包的用途

闭包有两个常用的用途:

  • 封装私有变量
  • 做缓存

封装私有变量

闭包可以使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量,以防止其被外部访问和修改。

在下面这个例子中,调用函数,输出的结果都是1,但是我们的代码效果是想让count每次加一。

function add() {
    let count = 0;
    count++;
    console.log(count);
}
add()   //输出1
add()   //输出1
add()   //输出1

一种显而易见的方法是将count提到函数体外,作为全局变量。这么做当然是可以解决问题,但是在实际开发中,一个项目由多人共同开发,你不清楚别人定义的变量名称是什么,很容易冲突,有什么其他的办法可以解决这个问题呢?

function add(){
    let count = 0
    function a(){
        count++
        console.log(count);
    }
    return a
}
var res = add() 
res() //1 
res() //2
res() //3

答案是用闭包。在上面的代码示例中,add函数返回了一个闭包a,其中包含了count变量。由于count只在add函数内部定义,因此外部无法直接访问它。但是,由于a函数引用了count变量,因此count变量的值可以在闭包内部被修改和访问。这种方式可以用于封装一些私有的数据和逻辑。

做缓存

函数一旦被执行完毕,其内存就会被销毁。而闭包可以使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

function foo(){
  var myName ='张三';
  let test = 1;
  var innerBar={
      getName: function(){
          console.log(test);
          return myName;
      },
      setName:function(newName){
          myName = newName;
      }
  }
  return innerBar;
}
var bar = foo();
console.log(bar.getName()); //1 张三
bar.setName('李四');
console.log(bar.getName()); //1 李四

这里var bar = foo() 执行完后本来应该被销毁,但是因为形成了闭包,所以导致foo执行上下文没有被销毁干净,被引用了的变量myName、test没被销毁,闭包里存放的就是变量myName、test,这个闭包就像是setName、getName的专属背包,setName、getName依然可以使用foo执行上下文中的test和myName。

闭包的应用是非常广泛的,比如常见的防抖和节流等其实也都是闭包的应用。

闭包的缺点

闭包也存在着一个潜在的问题,由于闭包会引用外部函数的变量,但是这些变量在外部函数执行完毕后没有被释放,那么这些变量会一直存在于内存中,这可能会带来内存泄漏问题,因此,需要及时释放闭包,即手动调用闭包函数,并将其返回值赋值为null,这样可以让闭包中的变量及时被垃圾回收器回收。

结语

本文主要介绍了被誉为JavaScript中最难理解的概念之一的闭包,闭包的表现形式多样、应用广泛,日常开发中其实都有闭包的身影,在实际的开发过程中,合理地使用闭包可以帮助我们更加高效地编写代码,提高程序的性能和可维护性。

如果此文对你有帮助的话,欢迎关注点赞、⭐收藏✍️评论支持一下博主~ 

你可能感兴趣的:(javascript,javascript,前端)