闭包是什么,有什么用途

引言:

闭包是JavaScript中老生常谈的一个话题,也是常见的面试题。

我之前刚开始做前端的时候,就在网上搜闭包,大多搜到的结果都说的是闭包有什么作用,比如说在函数外部可以访问到函数内部的变量,又或者说闭包会导致什么问题,比如会影响GC回收。总之好像没什么标准说法,所以当时我总是模模糊糊的。

概念

其实闭包不止存在于JavaScript中,查阅wiki我们可以看到,闭包这个概念存在于计算机编程中,以下是关于它的一段概述:

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment.[1] The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.[b] Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

翻译过来的意思是:

在编程语言中,闭包(也称词法闭包或函数闭包)是一种在支持一级函数的语言中实现词法作用域名称绑定的技术。从操作上讲,闭包是将函数和环境存储在一起的记录。环境是一个映射,它将函数的每个自由变量(在本地使用但在外层作用域中定义的变量)与创建闭包时名称绑定的值或引用关联起来。与普通函数不同,闭包允许函数通过闭包的值或引用的副本去访问这些捕获的变量,即使函数在它们的作用域之外被调用。

从上述概述中,我们可以有以下理解:

  1. 实现闭包的前提

    需要编程语言支持一级函数

  2. 闭包是一种技术

    实现了词法作用域名称绑定

  3. 如何实现闭包

    将函数和环境存储在一起

  4. 闭包的相关术语

    环境:一种映射,将自由变量与值或引用相关联

    自由变量

    In computer programming, the term free variable refers to variables used in a function that are neither local variables nor parameters of that function. The term non-local variable is often a synonym in this context.

    在计算机编程中,自由变量一词指函数中使用的变量,这些变量既不是局部变量,也不是函数的参数。在这种情况下,非局部变量通常是一个同义词。
    
  5. 闭包的作用

    允许函数通过闭包的值或引用的副本去访问捕获的变量,即使函数在它们的作用域之外被调用

那什么样的语言属于支持一级函数呢,wiki中也有简单的描述:

Closures typically appear in languages with first-class functions—in other words, such languages enable functions to be passed as arguments, returned from function calls, bound to variable names, etc., just like simpler types such as strings and integers.

语言中允许函数作为参数传递、从函数调用中返回、绑定到变量名等等,就像字符串和整数等简单类型一样。

毫无疑问,根据这段描述,JavaScript就是支持一级函数的编程语言。

JS中的闭包

以上是Wiki中对闭包的定义,在JavaScript高级程序设计这本书中,给出的是一段比较简短的定义:

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

对于这个简短的句子,我们可以做以下理解:

  1. 闭包是函数
  2. 这个函数持有对另一个函数作用域中变量的引用
  3. 通过嵌套函数实现

关键词

根据以上内容,我们可以抓住几个与闭包相关的关键词:

第一,函数;第二,作用域;第三,自由变量。

作用域链

众所周知,在JS中执行一个函数时,会在函数内部形成一个新的作用域,在这个作用域中可以访问这个函数的外部作用域中的变量和函数,这就会形成作用域链,在正常情况下,外部作用域是访问不到这个函数内部作用域的变量和函数的。

在函数执行时,会沿着作用域链去查找变量,以便读、写值。

活动对象

作用域链其实是一个包含指针的列表,其中每个指针分别指向一个活动对象。

每个函数执行时,其执行上下文中都会有一个对象,这个对象包含了其内部的变量,在全局上下文中的叫变量对象,会在代码执行期间始终存在;而在函数局部上下文中的叫做活动对象,只在函数执行期间存在。

例子

关于普通函数的作用域链和活动对象的创建和销毁,可以参考下面这个例子:

function compare(value1, value2) {
  if(value1 < value2) {
    return -1;
  } else if(value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
let result = compare(5, 10);
  • 在定义compare()函数时,会预装载全局变量对象,并保存在内部的[[Scope]]中;

  • 在执行compare()函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链;接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

函数执行完毕后,局部的活动对象会被销毁,内部作用域会被销毁,内存中就只剩下全局作用域。

对比来说,生成闭包后就没这么简单了,被闭包持有引用的函数,在执行完毕后,其活动对象和作用域通常无法立即被销毁,因为函数的局部变量都存在于其活动对象上,闭包持有了函数变量的引用,就相当于持有了其活动对象的引用,也就是说活动对象还在使用中,不能被销毁。

比如下面这个定时器的例子:

setTimeout(() => console.log(2), 1000);

这个返回console.log的匿名函数是setTimeout这个函数的局部变量,存在于其活动对象上,众所周知,在setTimeout函数执行完毕后,匿名函数并不会立即执行,而是会被推入浏览器的任务队列,持有这个匿名函数引用的函数就是一个闭包函数。

所以setTimeout执行完毕后,其作用域不会被立即销毁,当这个匿名函数被出列执行后,setTimeout的作用域才会被销毁。

存在的问题

根据上述的例子,我们可以看出,闭包最主要存在的一个问题,就是影响内存回收,可能造成内存泄漏,所以建议仅在十分必要时使用。

应用

虽然闭包存在的问题很明显,但在JavaScript中的使用还是很普遍的,因为动态网页中经常需要处理用户交互,加上JavaScript的单线程,需要使用回调来处理事件。

除了应用在用户交互以外,闭包还可以有以下的应用:

  1. 防止变量外溢,全局污染

    在前端开发中,我们经常会使用一些三方库,如果没有经过封装,三方库很可能会与业务代码产生冲突,此时可以借助立即执行函数IIFE来封装第三方库,将必要的变量作为函数的返回,或者放在某个特定的全局变量上,这样其他内部变量就不会暴露出来,与业务代码冲突。

    除了第三方库,这在团队协作中也很有用,防止与团队内部其他人的代码造成命名冲突。

  2. 访问函数的“私有”变量

    通常函数本身就可以用于封装代码,有时候我们想更规范代码,将相关业务的代码放在一起,此时可以通过函数来将这些代码集合起来,这样外部也不能轻易修改函数内部的变量数据,保障了数据的安全性。

    但有时我们需要访问函数内部的私有变量,此时可以通过创建能够访问函数私有变量/函数的方法,并对外暴露此方法的引用创建闭包,从而达到访问内部私有变量的目的。

  3. 实现对象属性私有化

    在JavaScript中,对象没有私有属性这种设定,只要知道对象有某个属性,就可以通过点操作符来访问属性的值,在某些场景下,我们不希望外部直接访问对象的属性,甚至不希望某些属性能被访问到,此时就可以通过闭包来实现类似私有的属性。

    比如定义一个函数,将这个函数内部的某些变量当作对象的私有属性,最后返回一个对象。通过调用这个函数,就可以拿到一个对象,如果函数中的某些变量不想被直接访问,可以不放到这个对象上,如果可以被读取,就可以在对象上放一个读取的公有方法。这样就实现了对象属性私有化。

  4. 函数之间私下通信

    如果两个闭包持有某个相同的引用,它们就可以通过更新这个引用来进行私下通信。

  5. 实现模块化

    很久之前编写JavaScript代码,通常都是很零碎的,会导致代码很分散,不好维护,难以复用,通过闭包我们可以实现模块化,而模块化是工程化的基础,可以很大地提高工作效率和合作效率。

    通常一个模块提供对一种业务功能的支持,使用对象来表示,这个对象可以看作是一堆变量和函数的集合。

    模块可能有一些初始化操作,以及有一些不想暴露给外部的私有变量/函数,可以通过立即执行函数,将这些初始化操作和变量/函数限制在内部作用域,在IIFE执行后返回一个模块对象,在这个对象上可以暴露一些公有属性和公有方法。

总结

最后来总结一下,虽然闭包存在很明显的问题,但是JavaScript作为单线程模型的语言,不太可能不用闭包,最好就是用完能及时销毁引用。

posted @ 2022-11-11 13:05  beckyye  阅读(239)  评论(0编辑  收藏  举报