闭包是一个学起来令人困惑的 JavaScript 概念,因为很难看出它们是如何被实际使用的。

与函数、变量和对象等其他概念不同,你并不总是直接地使用闭包。你不会说:哦! 在这里,我将使用闭包作为解决方案。

但与此同时,你可能已经使用过这个概念一百次。学习闭包更多的是确定何时使用一个闭包,而不是学习一个新概念。

JavaScript 中的闭包是什么

当函数读取或修改在其上下文之外定义的变量的值时,就有了一个闭包。

const value = 1
function doSomething() {
    let data = [1,2,3,4,5,6,7,8,9,10,11]
    return data.filter(item => item % value === 0)
}

这里函数 doSomething 使用变量 value。但是函数 item => item % value === 0 也可以被写成:

function(item){
    return item % value === 0
}

你使用在函数外部定义的变量 value 的值。

函数读取外部的值

与前面的示例一样,函数可以访问和使用在其“主体”或上下文之外定义的值,例如:

let count = 1
function counter() {
    console.log(count)
}
counter() // print 1
count = 2
counter() // print 2

这允许我们从模块中的任何地方修改 count 变量的值。那么当计数器函数被调用时,它就会知道如何使用当前的值。

我们为什么使用函数

但是为什么我们要在程序中使用函数呢?当然,在不使用我们定义的函数的情况下编写程序是可能的——困难,但可能。那么我们为什么要创建适当的函数呢?

想象一段代码,它做了一些很棒的事情,它有 X 行代码。

/* My wonderful piece of code */

现在假设你必须在程序的各个部分使用这段精彩的代码,你会怎么做?

“自然”的选择是将这段代码放在一个可以重用的集合中,这个可重用的集合就是我们所说的函数。函数是在程序中重用和共享代码的最佳方式。

现在,你可以尽可能多地使用你的函数。而且,忽略某些特殊情况,调用你的函数 N 次与编写那段优美的代码 N 次是一样的。这是一个简单的替换。

在哪里使用闭包呢

使用计数器的示例,让我们将它当作那段优美的代码

let count = 1
function counter() {
    console.log(count)
}
counter() // print 1

现在,我们想在很多地方重用它,所以我们将它“包装”在一个函数中。

function wonderfulFunction() {
    let count = 1
    function counter() {
        console.log(count)
    }
    counter() // print 1
}

现在我们有什么?一个函数:counter,使用在它之外声明的值 count。还有一个值:count,它是在 wonderfulFunction 函数作用域中声明的,但在 counter 函数中使用。

也就是说,我们有一个函数,它使用了一个在其外部声明的值:一个闭包

很简单,不是吗?现在,当执行 WonderFunction 函数时会发生什么?一旦父函数被执行,变量 count 和函数 counter 会发生什么变化?

在其主体中声明的变量和函数会“消失”(垃圾收集器)。

现在,让我们稍微修改一下示例:

function wonderfulFunction() {
    let count = 1
    function counter() {
        count++
        console.log(count)
    }
   setInterval(counter, 2000)
}
wonderfulFunction()

wonderfulFunction 内部声明的变量和函数现在会发生什么?

在这个例子中,我们告诉浏览器每 2 秒运行一次 counter。因此,JavaScript 引擎必须保留对函数及其使用的变量的引用。即使在父函数 wonderFunction 完成其执行周期后,函数 counter 和值 count 仍将“存活”。

之所以会出现这种具有闭包的“效果”,是因为 JavaScript 支持函数的嵌套。或者换句话说,函数是语言中的一等公民,你可以像使用任何其他对象一样使用它们:嵌套、作为参数传递、作为返回值等等。

我可以在 JavaScript 中使用闭包做什么

立即调用函数表达式IIFE

这是一种在 ES5 时代被大量使用以实现“模块”设计模式的技术(在此之前被原生支持)。这个想法是将你的模块“包装”在一个立即执行的函数中。

(function(arg1, arg2){
...
...
})(arg1, arg2)

这使你可以使用只能由模块本身在函数内使用的私有变量——也就是说,允许模拟访问修饰符。

const module = (function(){
	function privateMethod () {
	}
	const privateValue = "something"
	return {
	  get: privateValue,
	  set: function(v) { privateValue = v }
	}
})()

var x = module()
x.get() // "something"
x.set("Another value")
x.get() // "Another Value"
x.privateValue //Error

函数工厂

另一个通过闭包实现的设计模式是“函数工厂”,即函数创建函数或对象,例如,允许你创建用户对象的函数。


const createUser = ({ userName, avatar }) => ({
      id: createID(),
      userName,
      avatar,
      changeUserName (userName) {
        this.userName = userName;
        return this;
      },
      changeAvatar (url) {
        // execute some logic to retrieve avatar image
        const newAvatar = fetchAvatarFromUrl(url)
        this.avatar = newAvatar
        return this
      }
    });
    
        console.log(createUser({ userName: 'Bender', avatar: 'bender.png' }));
    
    {
      "id":"17hakg9a7jas",
      "avatar": "bender.png",
      "userName": "Bender",
      "changeUsername": [Function changeUsername]
      "changeAvatar": [Function changeAvatar]
    
    }
    */c

使用这种模式,你可以实现函数式编程中的一个想法,称为柯里化

柯里化

柯里化是一种设计模式(也是某些语言的特征),立即评估一个函数并返回第二个函数。

你可以使用闭包创建这些“柯里化”函数,定义并返回闭包的内部函数。

function multiply(a) {

    return function (b) {
        return function (c)  {
            return a * b * c
        }
    }
}
let mc1 = multiply(1);
let mc2 = mc1(2);
let res = mc2(3);
console.log(res);

let res2 = multiply(1)(2)(3);
console.log(res2);

这些类型的函数采用单个值或参数并返回另一个也接收一个参数的函数。它是参数的部分应用。也可以使用 ES6 重写此示例。

let multiply = (a) => (b) => (c) => {

    return a * b * c;
}

let mc1 = multiply(1);
let mc2 = mc1(2);
let res = mc2(3);
console.log(res);

let res2 = multiply(1)(2)(3);
console.log(res2);

我们可以在哪里应用柯里化?在组合中,假设你有一个创建 HTML 元素的函数。

function createElement(element){
    const el = document.createElement(element)
    return function(content) {
        return el.textNode = content
    }
}

const bold = crearElement('b')
const italic = createElement('i')
const content = 'My content'
const myElement  = bold(italic(content)) // <b><i>My content</i></b>

事件监听器

另一个可以使用和应用闭包的地方是使用 React 的事件处理程序。

假设你使用第三方库来呈现数据集合中的项目。这个库有一个名为 RenderItem 的组件,它只有一个可用的 prop,onClick。这个 prop 不接收任何参数,也不返回值。

现在,在你的特定应用程序中,你要求当用户单击该项目时,该应用程序显示带有该项目标题的警报。但是你的 onClick 事件不接受参数——那么你能做什么呢?闭包可以发挥作用了:

// Closure
// with es5
function onItemClick(title) {
    return function() {
      alert("Clicked " + title)
    }
}
// with es6
const onItemClick = title => () => alert(`Clcked ${title}`)

return (
  <Container>
{items.map(item => {
return (
   <RenderItem onClick={onItemClick(item.title)}>
    <Title>{item.title}</Title>
  </RenderItem>
)
})}
</Container>
)

在这个简化的示例中,我们创建了一个函数,该函数接收你要显示的标题,并返回另一个函数,满足 RenderItem 作为 prop 接收的函数定义。

总结

你甚至可以在不知道你正在使用闭包的情况下开发应用程序。但是,当你创建解决方案时,了解它们的存在以及它们的实际工作方式会开启新的可能性。

闭包是你刚开始时可能难以理解的概念之一。但是一旦你知道你正在使用它们,并理解它们,它就可以增加你的工具,并推进你的职业生涯。

English-Footer-Social-Card-1

🐦 在 Twitter 关注我          ✉️ 订阅邮件           ❤️ 支持我的工作

原文:How to Use Closures in JavaScript – A Beginner's Guide,作者:Matías Hernández