如何学习 JavaScript 中的闭包并了解何时使用它们
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
如果你像我一样,听到过诸如词法环境、闭包、执行上下文之类的概念,你会觉得“是的,我听说过”,就是想不起来它们是什么,但我可能正在用它们。你知道吗,你是对的。你很可能正在用它们,但谁又能记住这些术语呢?
我的意思是,我们很可能只有在准备 JavaScript 面试的时候才需要知道这些术语的名称。我不是说不要学习这些概念,而是说只要你知道它们的工作原理,即使你给它们起了个不同的名字,世界也不会崩溃:
我们知道在面试时我们需要了解这些术语,其余时间我们只需要知道编码时事情是如何运作的,我们也确实这样做了。
让我们深入探讨一下,为什么我们能理解甚至运用这些术语,却不知道它们叫什么?是命名不当吗?或许,就我而言,我意识到自己是一个视觉学习者,我需要借助图像来记忆,否则记忆力就无法持久。
闭包
什么是闭包?闭包是与其词法环境捆绑在一起的函数。
谢谢教授,但我几乎听不懂一个字。
好的,让我们看一些代码:
function outer() {
// lexical environment
let a = 1;
return function inner(b) {
return a + b
}
}
上面你看到的是一个函数outer()
包裹着另一个函数inner
。它不仅包裹着函数 inner()
,还包裹着变量a
。
这有什么了不起的?
即使在函数outer()
停止执行之后,函数inner()
仍然可以访问其词法环境,在本例中为变量a
。
词汇环境,听起来像词典,又大又重的书。给我看看。
好的,想象一下我们这样调用代码:
const fn = outer();
fn(5) // 6
上面它记得 a
有价值1
。
好的,所以它就像是
a
一个私有变量,但是在函数中?
是的,确实如此。
我知道该如何记住这一点:)
是的?
奶牛
牛?!
是的,奶牛在一个封闭的环境中,外部函数作为封闭体,奶牛作为内部函数和私有变量,如下所示:
噢,慢慢地走开了。
我们可以用它们来做什么
好的,我们对闭包有了一些介绍,但让我们说明一下我们可以用它们来做什么:
- 通过创建私有变量,我们可以在外部函数执行完毕后很长时间内创建一个词法环境,这使得我们可以将词法环境视为类中的私有变量。这样我们就可以编写如下代码:
function useState(initialValue) {
let a = initialValue;
return [ () => a, (b) => a = b];
}
const [health, setHealth] = useState(10);
console.log('health', health()) // 10
setHealth(2);
console.log('health', health()) // 2
a
上面我们看到了如何返回一个数组,该数组公开了从词法环境返回和设置变量的方法
- 部分应用,其思想是接受一个参数但不完全应用它。我们在第一个例子中已经展示了这一点,但让我们展示一个更通用的方法
partial()
:
const multiply = (a, b) => a * b;
function partial(fn, ...outer) {
return function(...inner) {
return fn.apply(this, outer.concat(inner))
}
}
const multiply3 = partial(multiply, 3);
console.log(multiply3(7)) // 21
上面的代码收集了第一个函数的所有参数outer
,然后返回内部函数。接下来,你可以调用返回值,因为它是一个函数,如下所示:
console.log(multiply3(7)) // 21
好的,我想我明白了它的工作原理。那么应用程序呢?我什么时候真正使用它呢?
嗯,它有点像学术构造,但它肯定在库和框架中使用。
就是这样?
我的意思是,您可以使用它使功能更加专业化。
仅举一个例子?
当然,这里有一个:
const baseUrl = 'http://localhost:3000';
function partial(fn, ...args) {
return (...rest) => {
return fn.apply(this, args.concat(rest))
}
}
const getEndpoint = (baseUrl, resource, id) => {
return `${baseUrl}/${resource}/${id ? id: ''}`;
}
const withBase = partial(getEndpoint, baseUrl);
const productsEndpoint = withBase('products')
const productsDetailEndpoint = withBase('products', 1)
console.log('products', productsEndpoint);
console.log('products detail', productsDetailEndpoint);
上面是一个相当常见的场景,构建一个 URL 端点。我们创建了一个更特殊的版本withBase
,部分应用了baseUrl
。然后我们继续添加具体的资源,如下所示:
const productsEndpoint = withBase('products')
const productsDetailEndpoint = withBase('products', 1)
它不是必须的,但它很有用,可以减少代码的重复性。它是一种模式。
- 隔离部分代码/通过 JavaScript 面试,为此,我们先来展示一个在 JS 面试中很常见的问题。我连续三次面试都被问到同样的问题。如果你用 Google 搜索一下,也能找到这个问题。你猜怎么着,JavaScript 面试流程有问题。
什么意思?坏了?
没人会在意你是否有多年的经验,或者了解很多框架。面试官通常会花 5 分钟在谷歌上搜索 JavaScript 问题来问你。
听起来他们问的是 JavaScript 语言及其核心概念。不是很好吗?
是的,这部分不错,但是 JavaScript 本身就有很多怪异之处,所以 Crockford 写了一本名为《JavaScript 的优秀之处》的书,这本书很薄,是有原因的。这本书确实有优点,但也有很多怪异之处。
你要告诉我一个面试问题吗?
好的,这是代码,你能猜出答案吗?
for (var i = 0; i < 10; i++) {
setTimeout(() => {
return console.log(`Value of ${i}`);
}, 1000)
}
1,2,3,4,5,6,7,8,9,10
沒有受傷。
好冷啊,能告诉我为什么吗?
setTimeout
是异步的,并在几毫秒后调用1000
。for 循环会立即执行,因此在setTimeout
调用时i
,参数将达到最大值10
。因此它会打印10
,10
次。但我们可以修复它,使其以升序打印。
如何?
通过创建范围,在代码中实现隔离,如下所示:
for (var i = 0; i < 10; i++) {
((j) => setTimeout(() => {
return console.log(`Value of ${j}`);
}, 1000))(i)
}
上面创建了一个立即调用的函数表达式,IIFE(它看起来确实有点不确定,对吧;)?)。它实现了隔离,其中的每个值都绑定到特定的函数定义和执行。i
除了上述解决方案之外,还有一种替代方案,那就是使用let
。该let
关键字会创建一个作用域代码块。因此,代码应该如下所示:
for (let i = 0; i < 10; i++) {
setTimeout(() => {
return console.log(`Value of ${i}`);
}, 1000)
}
感谢 Quozzo 指出这一点。
概括
好吧,所以整个封闭事件都与奶牛、栅栏和隐私有关
还有 JavaScript ;)
链接:https://dev.to/itnext/how-you-can-learn-closures-in-javascript-and-understand-when-to-use-them-2lk5