闭包是什么?闭包的用途是什么?

简介: 闭包是什么?闭包的用途是什么?

🚀 作者简介:程序员小豪,全栈工程师,热爱编程,曾就职于蔚来,现就职于某互联网大厂,技术栈:Vue、React、Python、Java

🎈 本文收录于小豪的前端系列专栏,后续还会更新前端入门以及前端面试的一些相关文章,手把手带你从零学习前端到面试找工作,并如果有想进入前端领域工作的同学,这个前端专栏会对你有所帮助,欢迎关注起来呀

🌼 本人也会持续的去关注AIGC以及人工智能领域的一些动向并总结到博客中,大家感兴趣的可以关注一下我的人工智能专栏

🌊 云原生的入门学习系列,大家有兴趣的可以看一看

本文目录

什么是闭包


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

因为内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,这个时候就形成了闭包。

所以闭包通常是在函数嵌套中形成的。

例如下面的代码,就形成了闭包:

javascript
复制代码function foo (){
  var name = 'snail'
  return function(){
    console.log('my name is '+name)
  }
}
var bar = foo();
bar();

闭包并不是一个需要学习新的语法或模式才能使用的工具或技巧,它是基于词法作用域编写代码时自然产生的结果。你甚至不需要为了闭包而创建闭包,了解了闭包,你会发现,代码中闭包随处可见。

了解闭包是为了可以根据自己的意愿来识别和影响闭包的使用。接下来我们看一下闭包常见的应用场景。

应用场景


实现块级作用域

首先我们来看这样一段代码:

function foo(){
  var result = [];
  for(var i = 0;i<10;i++){
    result[i] = function(){
      console.log(i)
    }
  }
  return result;
}
var result = foo();
result[0](); // 10
result[1](); // 10

可以看到,每个函数并不像我们期待的那样 result[0]() 打印 0,result[1]() 打印 1,以此类推。

因为 var 声明的 i 不只是属于当前的每一次循环,甚至不只是属于当前的 for 循环,因为没有块级作用域,变量 i 被提升到了函数 foo 的作用域中。所以每个函数的作用域链中都保存着同一个变量 i,而当我们执行数组中的子函数时,此时 foo 内部的循环已经结束,此时 i = 10,所以每个函数调用都会打印 10。

接下来我们对 for 循环内部添加一层即时函数(又叫立即执行函数 IIFE),形成一个新的闭包环境,这样即时函数内部就保存了本次循环的 i,所以再次执行数组中子函数时,结果就像我们期望的那样 result[0]() 打印 0,result[1]() 打印 1 …

function foo(){
  var result = [];
  for(var i = 0;i<10;i++){
    (function(i){
      result[i] = function(){
        console.log(i)
      }
    })(i)
  }
  return result;
}
var result = foo();
result[0](); // 0
result[1](); // 1

保存内部状态

首先我们来看这样一段代码:

function cacheCalc(){
  var cache = new Map()
  return function (i){
    if(!cache.has(i)) cache.set(i,i*10)
    return cache.get(i)
  }
}
var calc = cacheCalc()
console.log(calc(2)) // 20

可以看到,函数内部会使用 Map 保存已经计算过的结果(当然也可以是其他的数据结构),只有当输入数字没有被计算过时,才会计算,否则会返回之前的计算结果,这样就会避免重复计算。

而这样的技巧在 Vue3源码 中同样有使用到。代码地址

这里我在阅读源码的过程中加了一些注释,导致截图中代码行号和源文件中的不一致,但是代码并未进行任何修改。

这里的 compileToFunction 函数会将我们编写的模板进行编译生成 render 函数,而为了避免重复编译,这里在内部创建了一个 compileCache 对象保存编译过的数据。

函数柯里化

首先说一下什么是函数柯里化?

柯里化是把接收多个参数的函数变成接收单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数。

翻译成人话就是可以将一个接受多个参数的函数分解成多个接收单个参数的函数的技术,直到接收的参数满足了原来所需的数量后,才执行原函数的逻辑。

例如一个非常经典的面试题 => 实现 add(x)(y)(z) = x+y+z 中就用到了函数柯里化。代码如下:

function add(x){
  return function(y){
    return function(z){
      return x+y+z
    }
  }
}
console.log(add(1)(2)(3)) // 6

再比如我们有一个函数 foo,可以将输入的数字保留两位小数。此时我们需要一个函数,可以把输入数字保留两位小数并每隔三位添加一个逗号,这个时候就可以把函数 foo 引入进来,并在之前结果的基础上添加每隔三位添加逗号的功能。

单例模式

单例模式是一种常见的涉及模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:

// 单例模式
function Singleton(){
  this.data = 'singleton';
}
Singleton.getInstance = (function () {
  var instance;
  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      return instance;
    }
  }
})();
var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'

模拟私有属性

javascript 没有 java 中那种 publicprivate 的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:

// 模拟私有属性
function getGeneratorFunc () {
  var _name = 'John';
  var _age = 22;
  return function () {
    return {
      getName: function () {return _name;},
      getAge: function() {return _age;}
    };
  };
}
var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined

闭包的缺点


从上面的介绍中我们可以得知,闭包的使用场景非常广泛,那我们是不是可以大量使用闭包呢?不可以,因为闭包过度使用会导致性能问题,还是看之前演示的一段代码:

function foo() {
  var a = 2;
  function bar() {
    console.log( a );
  }
  return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包

乍一看,好像没什么问题,然而,它却有可能导致 内存泄露

我们知道,javascript 内部的垃圾回收机制用的是引用计数收集:即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为 0 的变量标记为失效变量并将之清除从而释放内存。

上述代码中,理论上来说, foo 函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo 运行完成以后,内部的变量就应该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个 baz 的变量在引用着 foo 内部的 bar 函数,这就意味着 foo 内部定义的 bar 函数引用数始终为 1,垃圾运行机制就无法把它销毁。更糟糕的是,bar 有可能还要使用到父作用域 foo 中的变量信息,那它们自然也不能被销毁… JS 引擎无法判断你什么时候还会调用闭包函数,只能一直让这些数据占用着内存。

这种由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露。

如何解决闭包导致的内存泄漏?

返回的函数调用后,把外部的引用关系置空

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null

总结


本期博客详细介绍了闭包是什么,闭包的应用场景,闭包的缺点以及如何解决闭包导致的内存泄漏问题,跟着这篇博文认真的学习下来相信下次面试官再问你闭包的问题你不会在惧怕,甚至能够回答的面面俱到让面试官眼前一亮。

相关文章
|
6月前
|
JavaScript 前端开发 数据安全/隐私保护
闭包对于保护私有变量和函数的作用
JavaScript中的闭包用于创建私有作用域,保护变量和函数不被外部直接访问。它们实现封装和信息隐藏,防止全局命名冲突,确保数据安全和稳定性。闭包还支持访问控制和持久状态保持,常用于模块化、数据隐藏等,增强代码的可维护性、可重用性和安全性。
|
6月前
|
缓存 JavaScript 前端开发
|
21天前
|
自然语言处理 JavaScript 前端开发
什么是闭包
【10月更文挑战第12天】什么是闭包
|
3月前
|
自然语言处理 JavaScript 前端开发
什么是闭包,它有什么作用
【8月更文挑战第2天】什么是闭包,它有什么作用
45 9
|
4月前
|
自然语言处理 JavaScript 前端开发
理解闭包的定义
【7月更文挑战第10天】闭包是编程中的关键概念,特别是函数式编程中。它是函数及其相关引用环境的组合,能访问词法作用域内外的变量,即使外部函数已执行完毕。闭包提供封装私有变量、保持状态、延迟执行的功能,常用于模块化、函数工厂、模拟私有方法和回调。JavaScript中通过函数嵌套实现闭包,但也可能导致内存泄漏和性能问题。
98 2
|
JavaScript 前端开发
对闭包的理解以及使用
对闭包的理解以及使用
48 1
|
存储 缓存 前端开发
详解 Reat 中的闭包问题
JavaScript 中的闭包一定是最可怕的特性之一。 即使是无所不知的 ChatGPT 也会告诉你这一点。 它也可能是最隐秘的语言概念之一。 每次编写任何 React 代码时,我们都会用到它,大多数时候我们甚至没有意识到。 但最终还是无法摆脱它们:如果我们想编写复杂且高性能的 React 应用程序,我们就必须了解闭包。
102 0
详解 Reat 中的闭包问题
什么是闭包?闭包的用途是什么?闭包的缺点是什么?
变量的作用域有两种:全局变量和局部变量; 函数内部可以直接读取全局变量; 在函数外部无法读取函数内的局部变量。 能够读取其他函数内部变量的函数,就是闭包
115 0
|
自然语言处理 JavaScript 前端开发
【闭包概念】关于闭包概念不同解读——你可以自己理解。
写在前面: 闭包是被讲烂的内容,但是当我不了解的情况下,看过很多教程,听过很多道理,还是无法完全理解闭包这个东西。所以想要写一篇比较详细,前端小白也能够真正理解闭包概念的干货文章,本文参考很多闭包资料,希望能真正把闭包这个东西讲清楚,喜欢的朋友可以点个赞,或者点波关注。 首先需要了解一波js变量的作用域以及变量的生存周期。 前几天我发在掘金上面了,下面是一个内容概要,墙裂推荐所有小伙伴,还是点进去学习一波,作用域这个问题可以说是闭包最为重要的内容。 里面的内容包括: 1.变量的作用域(变量的有效范围)。2.变量作用域,js高级程序设计(红宝书)中的解释。3.红宝书中的作用域链栗子(还有一个例子
192 0
【闭包概念】关于闭包概念不同解读——你可以自己理解。