由于闭包,回调、事件处理程序、高阶函数可以访问外部作用域变量。闭包在函数式编程中很重要,在 JavaScript 编码面试中经常被问到。
虽然到处都在使用,但闭包很难掌握。如果你还没有“啊哈!” 理解闭包的时刻,那么这篇文章适合你。
我将从基本术语开始:作用域和词法作用域。然后,在掌握了基础知识之后,您只需一步即可最终理解闭包。
在开始之前,我建议您抵制跳过作用域和词法作用域部分的冲动。这些概念对于闭包至关重要,如果你掌握了它们,闭包的想法就变得不言而喻了。
一、作用域
当您定义一个变量时,您希望它存在于某些边界内。例如,result变量calculate()作为内部细节存在于函数中是有意义的。在 之外calculate(),该result变量是无用的。
变量的可访问性由作用域管理。您可以自由访问在其作用域内定义的变量。但是在该作用域之外,该变量是不可访问的。
在 JavaScript 中,作用域由函数或代码块创建。
让我们看看作用域如何影响变量的可用性count。此变量属于 function 创建的作用域foo():
function foo() {
// The function scope
let count = 0;
console.log(count); // logs 0
}
foo();
console.log(count); // ReferenceError: count is not defined
count作用域内可以自由访问foo()。
但是,在foo()作用域之外,count是无法访问的。如果您count无论如何都尝试从外部访问,JavaScript 会抛出ReferenceError: count is not defined.
如果您在函数或代码块内定义了一个变量,那么您只能在该函数或代码块内使用该变量。上面的示例演示了这种行为。
现在,让我们看一个通用的公式:
作用域是一个空间策略,它规定了变量的可访问性。
一个直接的特性出现了——作用域隔离了变量。这很好,因为不同的作用域可以有同名的变量。
您可以重用公共变量的名称(count,index,current,value在不同的作用域,等等)不冲突。
foo()和bar()函数作用域有自己的但同名的变量count:
function foo() {
// "foo" function scope
let count = 0;
console.log(count); // logs 0
}
function bar() {
// "bar" function scope
let count = 1;
console.log(count); // logs 1
}
foo();
bar();
count来自foo()和bar()函数作用域的变量不会发生冲突。
2. 作用域嵌套
让我们更多地使用作用域,并将一个作用域嵌套到另一个作用域中。例如,函数innerFunc()嵌套在外部函数中outerFunc()。
2 个函数作用域将如何相互交互?我可以访问变量outerVar的outerFunc()从内部innerFunc()作用域?
让我们在示例中尝试一下:
function outerFunc() {
// the outer scope
let outerVar = 'I am outside!';
function innerFunc() {
// the inner scope
console.log(outerVar); // => logs "I am outside!"
}
innerFunc();
}
outerFunc();
事实上,outerVar变量在innerFunc()作用域内是可以访问的。外部作用域的变量可以在内部作用域内访问。
现在你知道了两件有趣的事情:
- 作用域可以嵌套
- 外部作用域的变量可以在内部作用域内访问
3. 词法作用域
如何JavaScript的理解outerVar里面innerFunc()相当于可变outerVar的outerFunc()?
JavaScript 实现了一种名为词法作用域(或静态作用域)的作用域机制。词法作用域意味着变量的可访问性由嵌套作用域内变量的位置决定。
更简单的是,词法作用域意味着在内部作用域内您可以访问外部作用域的变量。
之所以称为词法(或静态),是因为引擎仅通过查看 JavaScript 源代码而不执行它来确定(在词法分析时)作用域的嵌套。
词法作用域的提炼思想:
词法作用域由静态确定的外部作用域组成。
例如:
const myGlobal = 0;
function func() {
const myVar = 1;
console.log(myGlobal); // logs "0"
function innerOfFunc() {
const myInnerVar = 2;
console.log(myVar, myGlobal); // logs "1 0"
function innerOfInnerOfFunc() {
console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0"
}
innerOfInnerOfFunc();
}
innerOfFunc();
}
func();
innerOfInnerOfFunc()的词法作用域由innerOfFunc()、func()和全局作用域(最外层作用域)组成。在innerOfInnerOfFunc()中,你可以访问词法范围变量myInnerVar、myVar和myGlobal。
innerFunc()的词法范围包括func()和全局范围。在innerOfFunc()中,你可以访问词法范围的变量myVar和myGlobal。
最后,func()的词法范围只包括全局范围。在func()中,你可以访问词法范围变量myGlobal。
4. 闭包
好的,词法作用域允许静态访问外部作用域的变量。距离闭包只差一步!
让我们再看一下outerFunc()和innerFunc()示例:
function outerFunc() {
let outerVar = 'I am outside!';
function innerFunc() {
console.log(outerVar); // => logs "I am outside!"
}
innerFunc();
}
outerFunc();
在innerFunc()作用域内,变量outerVar是从词法作用域访问的。这已经知道了。
请注意,innerFunc()调用发生在其词法范围( 的范围outerFunc())内。
让我们做一个改变:innerFunc()在它的词法范围之外调用:在一个函数中exec()。将innerFunc()仍然能够访问outerVar?
让我们对代码片段进行调整:
function outerFunc() {
let outerVar = 'I am outside!';
function innerFunc() {
console.log(outerVar); // => logs "I am outside!"
}
return innerFunc;
}
function exec() {
const myInnerFunc = outerFunc();
myInnerFunc();
}
exec();
现在innerFunc()在其词法范围之外执行,但恰好在exec()`函数范围内。重要的是:
innerFunc()仍然可以访问outerVar其词法范围,即使在其词法范围之外执行。
换句话说,从其词法作用域中innerFunc() 闭包(也就是捕获、记住)变量outerVar。
换句话说,innerFunc()是一个*闭包,*因为它outerVar从它的词法范围闭包了变量。
您已经完成了了解闭包是什么的最后一步:
闭包是一个访问其词法范围甚至在其词法范围之外执行的函数。
更简单的是,闭包是一个函数,它从定义它的地方记住变量,而不管它后面在哪里执行。
识别闭包的经验法则:如果在函数内部看到一个外来变量(未在该函数内定义),则该函数很可能是一个闭包,因为外来变量被捕获。
在前面的代码片段中,outerVar是innerFunc()从outerFunc()作用域捕获的闭包内的一个外来变量。
让我们继续举例说明为什么闭包是有用的。
5. 闭包示例
5.1 事件处理程序
让我们显示一个按钮被点击的次数:
let countClicked = 0;
myButton.addEventListener('click', function handleClick() {
countClicked++;
myText.innerText = `You clicked ${countClicked} times`;
});
打开演示并单击按钮。文本会更新以显示点击次数。
单击按钮时,handleClick()会在 DOM 代码内部的某处执行。执行发生在远离定义的地方。
但是作为一个闭包,它从词法作用域中handleClick()捕获countClicked并在点击发生时更新它。更何况,myText也被俘虏了。
5.2 回调
从词法范围捕获变量在回调中很有用。
一个setTimeout()回调函数:
const message = 'Hello, World!';
setTimeout(function callback() {
console.log(message); // logs "Hello, World!"
}, 1000);
Thecallback()是一个闭包,因为它捕获了变量message。
forEach()的迭代器函数:
let countEven = 0;
const items = [1, 5, 100, 10];
items.forEach(function iterator(number) {
if (number % 2 === 0) {
countEven++;
}
});
countEven; // => 2
这iterator是一个闭包,因为它捕获countEven变量。
5.3 函数式编程
当一个函数返回另一个函数直到参数被完全提供时,就会发生柯里化。
例如:
function multiply(a) {
return function executeMultiply(b) {
return a * b;
}
}
const double = multiply(2);
double(3); // => 6
double(5); // => 10
const triple = multiply(3);
triple(4); // => 12
multiply 是一个柯里化函数,它返回另一个函数。
柯里化是函数式编程的一个重要概念,由于闭包,它也成为可能。
executeMultiply(b)是一个a从其词法范围
六,结论
范围规则变量的可访问性。可以有一个函数或一个块作用域。
词法作用域允许函数作用域静态访问来自外部作用域的变量。
最后,闭包是一个从其词法范围捕获变量的函数。简单来说,闭包从定义它的地方开始记住变量,不管它在哪里执行。
闭包允许事件处理程序、回调来捕获变量。它们用于函数式编程。此外,在前端工作面试期间,您可能会被问及关闭如何运作。
每个 JavaScript 开发人员都必须知道闭包是如何工作的。处理它⌐■_■。
你还在为理解闭包而苦恼吗?如果是这样,请在下面的评论中问我一个问题!