原文:Closure in JavaScript – Explained with Examples,作者:Keyur Paralkar

这篇文章将讲解JavaScript中的闭包。我会说明闭包的定义,展现一个日常抓取使用闭包的例子以及闭包的优缺点。

目录

话不多说,我们开始吧!

前提条件

在学习这篇文章之前,你必须理解以下内容:

什么是闭包

闭包是即便外部函数已经不存在,也可以获取作用域链上变量的函数。

在解释闭包之前,我们先了解什么是作用域链。作用域链指的是在父作用域中无法访问子作用域中的变量,但是子作用域可以访问父作用域中的变量。

请看以下例子来获取更清晰的理解:

let buttonProps = (borderRadius) => {
	const createVariantButtonProps = (variant, color) => {
		const newProps = {
			borderRadius,
			variant,
			color
		};
		return newProps;
	}
	return createVariantButtonProps;
}

在这个例子中有一个名为 buttonProps的函数,形参为borderRadius。我们把buttonProps函数认定为父函数。

在父函数中我们定义了一个子函数,接受形参 createVariantButtonPropsvariantcolor ,并且返回一个包含 borderRadius变量的对象,这个变量位于子函数作用域外。

那么问题来了,内部函数如何解析位于父作用域的变量呢?

这得益于词法作用域。JS编译器可以根据词法作用域来解析当前作用域的变量,即解析嵌套函数中的变量。

所以根据上述例子,我们可以得出 createVariantButtonProps 可以获取外部函数buttonProps中的变量。

在这个例子中,内部函数createVariantButtonProps就是一个闭包。 想要深入了解闭包,我们必须先了解闭包的特性:

  • 即便外部函数不再存在,闭包也可以获取父函数中的变量。
  • 闭包不可以获取外部函数中的形参`args·。

让我们来深入了解这两个特性。

即便外部函数不再存在,闭包也可以获取父函数中的变量。

这是闭包的基础功能,是闭包的生命信仰,也可以说是闭包运作的指导原则。

我们现在执行buttonProps 函数,来看看闭包在实际中的运用:

let primaryButton = buttonProps("1rem"); 

调用buttonProps函数会返回另一个函数,即我们的闭包。

现在来执行闭包:

const primaryButtonProps = primaryButton("primary", "red");

闭包一旦执行,便返回下面的对象:

{
   "borderRadius":"1rem",
   "variant":"primary",
   "color":"red"
}

为什么 primaryButton函数可以获取并不位于其内部的 borderRadius 变量的值?

回看我们之前讨论的内容,会发现这个例子非常匹配闭包和作用域链的定义。

让我们深入理解为什么即便外部函数不存在了,闭包也可以获取父函数中的变量?例如,在这里获取了borderRadius变量的值。

答案很简单:闭包不存储静态值,相反,闭包存储作用域链上变量的引用。这样,即便外部函数被销毁,内部函数———即闭包仍能够获取父函数内的变量。

如何使用闭包

我们已经学习了什么是闭包,现在让我们创建一个可以被广泛使用的实用函数。这个函数将使用REST API来处理如GET和POST之类的方法。

在这个例子中:

  • 我们将利用JSON placeholder APIs的数据。我们可以使用REST API来处理一些虚构的数据。
  • 我们将使用JavaScript的fetch API。

我们先来讨论一下为什么要设计这样的实用函数,有以下几个原因:

  • 我们并不想在每一次fetch调用中都重新定义基础URL (或其他基础参数)。 所以我们可以创建一个机制将基础URL或者参数作为状态存储。
  • 删除多余的代码。
  • 模块化代码库。

让我们来看看这个实用函数的细节:

const fetchUtility = (baseURL, headers) => {
  const createFetchInstance = (route, requestMethod, data) => {
    const tempReq = new Request(`${baseURL}${route}`, {
      method: requestMethod,
      headers,
      data: data || null
    });
    return [fetch, tempReq];
  };

  return createFetchInstance;
};
  • fetchUtility接受两个参数 baseURLheaders。这两个参数会在后面闭包用到,来创建基础URL和头部。
  • 还有createFetchInstance函数,接受route requestMethoddata参数。
  • 接着在函数内创建一个请求实例,通过代码 ${baseURL}${route}来创建URL。同时我们也传入一个对象,包含请求方式、请求头和数据(如果获取得到数据)。
  • 然后返回一个fetch API的实例和一个请求对象。
  • 最后返回 createFetchInstance函数。

接下来让我们实际操作看看,调用 fetchUtility 函数初始化 baseURL:

const fetchInstance = fetchUtility("https://jsonplaceholder.typicode.com");

初始化基础URL

  • 仔细观察会发现fetchInstance获取了fetchUtility函数闭包的变量。
  • 然后,我们向闭包fetchInstance传入路由和请求的方式:
const [getFunc, getReq] = fetchInstance("/todos/1", "GET");

执行闭包

如你所见,一个包含fetch API实例和我们配置的请求体的数组被返回。

最后,我们使用getFunc这个fetch API 调用 getReq请求,如下:

getFunc(getReq)
  .then((resp) => resp.json())
  .then((data) => console.log(data));

我们可以一样的方法来创建一个POST请求。这样重新调用 fetchInstance

const [postFunc, postReq] = fetchInstance(
  "/posts",
  "POST",
  JSON.stringify({
    title: "foo",
    body: "bar",
    userId: 1
  })
);

在执行POST请求的时候也可以用GET请求一样的方法:

postFunc(postReq)
  .then((resp) => resp.json())
  .then((data) => console.log(data));

如果仔细观察上面的例子,我们会发现内部函数createFetchInstance 可以获取作用域链中的变量。 在词法作用域的帮助下,定义 createFetchInstance 时。它就可以解析作用域链上的变量。

也就是说,闭包在其定义过程中引用了变量 baseURLheaders。即便外部函数 fetchUtility 不复存在,这个引用依旧存在。

如果从另外一个角度来看闭包的话,闭包帮助我们保持如 baseURLheaders的状态,我们可以在函数其他地方调用。

闭包的优点

以下是闭包的优点:

  • 可以借助闭包在执行上下文中添加变量。
  • 可以使用闭包中的变量保存状态,之后使用。
  • 闭包提供数据封装。
  • 使用闭包可以删除多余的代码。
  • 可以使用闭包维护模块化代码。

闭包的缺点

过度使用闭包会引发两大问题:

  • 闭包内声明的变量不能被垃圾回收。
  • 应用中存在过多的闭包会影响应用运行速度,因为闭包会造成内存中保留重复的代码。

总结

如果你想要使用某种设计模式,闭包对你的帮助会很大。闭包还可以帮助你写出整洁、模块化的代码。

如果你对闭包感兴趣,我推荐你阅读以下话题:

感谢阅读!

你可以在 Twitter, GitHubLinkedIn上关注我。