【JavaScript】这次把闭包给你讲的明明白白

1,108 阅读6分钟

前言

有关闭包的文章,网上的讲解可以说是非常多,但并不是很容易读懂。一是因为闭包概念过于抽象;二是好多文章把闭包讲解的过于复杂。这篇文章讲解一下我对闭包的理解和总结,希望大家能够对闭包产生新的认识,如果有不对的地方欢迎指正。

全文概要

image.png

1 理解闭包

很多编程语言都支持闭包,闭包不是语言特性,而是一种编程习惯。闭包(Closure)是指具有一个封闭对外不公开包裹结构,或空间

在JS中,函数可以构成闭包,因为外部是不能访问到函数内部数据,对外封闭不公开,且函数通常是包裹一段代码。我们可以理解为闭包是函数在特定情况下执行产生的一种现象

那么怎样的情况会产生闭包呢?即产生闭包必满足三个条件:函数嵌套、内部函数引用外部函数数据、外部函数调用,凡是所有的闭包都满足以上三个条件,否则不构成闭包。

闭包到底是什么?我认为,所谓闭包,是一种引用关系,该引用关系存在内部函数中,内部函数引用外部函数数据,引用的数据可以在函数词法作用域(函数外部)之外使用。

2 解决的问题

我们都知道,根据作用域查找规则,外部是不能访问到函数内部数据,如果我们想访问函数内部数据,可以借助函数中的return

  function f1(){
    var num = 10;
    return num;
  }
  var res = f1();
  console.log(res); // 10

借助return可以访问到函数内部数据,但存在一个问题:数据不能被二次访问。因为第二次访问时候是再次调用该函数,函数中的代码才会再次返回,这个我们通过生成随机数可以很好的证明:

function f1() {
  var num = Math.random();
  return num;
}
var res = f1();
var res2 = f1();
console.log(res + '\n' + res2);

输出结果如下,我们发现两次的输出结果并不一样:

image.png

无论我们怎样执行,两次的随机数结果都不同,这种输出结果显然不好。如果我们想让函数只执行一次,我们该怎么做呢?我们可以f1函数中嵌套一个函数,嵌套的内部函数是可以访问f1函数变量的。

function f1(){
  var num = Math.random();
  
  function f2(){
    return num 
  }
  
  return f2
}

var f = f1();
var res1 = f();
var res2 = f();
console.log(res + '\n' + res2);

此时的输出结果如下:

image.png 这就产生了闭包,我们试着分析一下这段代码:

  • 全局f1函数在0级作用域链上,f1函数是一个一级链,f1函数中有一个变量num,还有一个函数体f2

  • f2是二级链,通过return将f2当做一个值返回给f1函数。

  • f1函数执行后,将f2的引用赋值给f,执行f函数,输出num变量。

正常来说,当f1函数调用完毕,其作用域是被销毁的,而通过闭包我们将f2给了ff2函数内仍然持对num的引用,num仍然存活内存中,延长了内部函数局部变量生命周期。在当f调用,num是可以访问到的。

image.png

其实,闭包也就是使用了链式访问技巧,0级链无法访问一级链数据,我们通过间接0级链操作二级链的函数,来访问一级链数据。

闭包解决的问题是:让函数外部访问到函数内部的数据。

3 闭包本质

再看上面这段代码,我们来分析闭包是如何产生的:

  function f1() {
    var num = Math.random();
    function f2() {
        return num;
    }
    
    return f2;
}

f1()

我们通过chrome调试工具查看这段代码,发现当代码运行到外部函数f1定义时,就会产生了闭包:

image.png

闭包(Closure)到底是个啥?闭包本质:

内部函数里的一个对象,这个对象非Js对象(有属性有方法的对象),这个对象是函数在运行时,本该释放的活动对象,这个活动对象里包含着我们引用的变量。

4 基本结构

上面我们说了,闭包就是间接获得函数内部数据使用权利,我们可以总结出常见的闭包结构。我们在日常开发中见到的绝大多数闭包结构,都是基于以下有三种。

4.1 return另一函数

写一个函数,函数内部定义一个新函数,返回新函数,用新函数获得函数内部数据。

function f1(){
  var a = 0
  function f2(){
    a++
    console.log(a)
  }
  return f2
}
var f = f1();
f(); // 1
f(); // 2

image.png

4.2 return绑定多个函数的对象

写一个函数,函数内定义一个对象,对象中绑定多个方法,返回对象,利用对象的方法访问函数内部数据。

eg : 如何获得超过一个数据?

function f1(){
  var num1 = Math.random();
  var num2 = Math.random();
  
  return{
    num1:function(){
      return num1;
    },
    num2:function(){
      return num2
    }
  }
}

f1()
// {num1: ƒ, num2: ƒ}

f = f1()
f.num1()
f.num2()

image.png

eg: 如何读取一个数据和修改一个数据?

function f1(){
  var num =Math.random();
  return {
    get_num:function(){
      return num;
    },
    set_num:function(value){
      // 此时num访问的是f1函数中的num
      num = value;
    }
  }
}

var f = f1();

// 读取函数中的值
var num = f.get_num();
console.log(num);
// 0.3919299622715364

// 设置函数中的值
f.get_num(123);
num = f.get_num();
console.log(num);
//123

image.png

4.3 将函数实参传递给另一函数

函数的实参,也就是函数中局部变量。

function delay(msg){
  setTimeout(function(){
    console.log(msg)
  },2000)
}
delay('开启计时器')

image.png

5 总结

  • 闭包:具有对外封闭不公开的包裹结构或空间,函数可以构成闭包。
  • 闭包产生三要素:函数嵌套、内部函数引用外部函数数据、外部函数调用。
  • 所谓闭包,是一种引用关系,该引用关系存在内部函数中,内部函数引用外部函数数据,引用的数据可以在函数词法作用域(函数外部)之外使用。
  • 解决的问题:间接访问函数中的数据、延长内部函数局部变量的生命周期。
  • 闭包本质:内部函数里的一个对象,这个对象非Js对象(有属性有方法的对象),这个对象是函数在运行时,本该释放的活动对象,这个活动对象里包含着我们引用的变量
  • 基本结构:rerurn另一函数、return绑定多个函数的对象、将函数实参传递给另一函数。

结语

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,写作不易,持续输出的背后是无数个日夜的积累,您的点赞是持续写作的动力,感谢支持。