范围和闭包在 JavaScript 中很重要。 但是,当我刚开始学习时,它们让我感到困惑。 以下是范围和闭包的解释,以帮助您理解它们是什么。
让我们从范围开始。
范围
JavaScript 中的范围定义了您可以访问哪些变量。 有两种范围 - 全局范围和局部范围。
全局范围
如果变量在所有函数或花括号 ({}
) 之外声明,则称其在全局范围内定义。
这仅适用于 Web 浏览器中的 JavaScript。 您在 Node.js 中以不同的方式声明全局变量,但本文不会讨论 Node.js。
const globalVariable = 'some value'
一旦您声明了全局变量,您就可以在代码中的任何地方使用它,甚至在函数中。
const hello = 'Hello CSS-Tricks Reader!'
function sayHello () {
console.log(hello)
}
console.log(hello) // 'Hello CSS-Tricks Reader!'
sayHello() // 'Hello CSS-Tricks Reader!'
虽然您可以在全局范围中声明变量,但建议不要这样做。 这是因为有命名冲突的可能性,即两个或多个变量具有相同的名称。 如果您使用const
或let
声明变量,则每当发生命名冲突时,您都会收到错误。 这是不可取的。
// Don't do this!
let thing = 'something'
let thing = 'something else' // Error, thing has already been declared
如果您使用var
声明变量,则第二个变量在声明后会覆盖第一个变量。 这是不可取的,因为它会使您的代码难以调试。
// Don't do this!
var thing = 'something'
var thing = 'something else' // perhaps somewhere totally different in your code
console.log(thing) // 'something else'
因此,您应该始终声明局部变量,而不是全局变量。
局部范围
仅在代码的特定部分可用的变量被认为是在局部范围中。 这些变量也被称为局部变量。
在 JavaScript 中,有两种局部范围:函数范围和块范围。
让我们先谈谈函数范围。
函数范围
当您在函数中声明一个变量时,您只能在函数内访问它。 一旦您退出函数,您就无法访问它。
在下面的示例中,变量hello
在sayHello
范围内
function sayHello () {
const hello = 'Hello CSS-Tricks Reader!'
console.log(hello)
}
sayHello() // 'Hello CSS-Tricks Reader!'
console.log(hello) // Error, hello is not defined
块范围
当您使用const
或let
在花括号 ({}
) 内声明变量时,您只能在该花括号内访问它。
在下面的示例中,您可以看到hello
的范围是花括号
{
const hello = 'Hello CSS-Tricks Reader!'
console.log(hello) // 'Hello CSS-Tricks Reader!'
}
console.log(hello) // Error, hello is not defined
块范围是函数范围的子集,因为函数需要使用花括号声明(除非您使用 带有隐式返回的箭头函数)。
函数提升和范围
函数在使用函数声明声明时,始终会被提升到当前范围的顶部。 因此,这两种方式是等效的
// This is the same as the one below
sayHello()
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
// This is the same as the code above
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
sayHello()
当使用函数表达式声明时,函数不会被提升到当前范围的顶部。
sayHello() // Error, sayHello is not defined
const sayHello = function () {
console.log(aFunction)
}
由于这两种变化,函数提升可能会让人困惑,不应使用。 始终在使用函数之前声明它们。
函数无法访问彼此的范围
当您分别定义函数时,函数无法访问彼此的范围,即使一个函数可能在另一个函数中使用。
在下面的示例中,second
无法访问firstFunctionVariable
。
function first () {
const firstFunctionVariable = `I'm part of first`
}
function second () {
first()
console.log(firstFunctionVariable) // Error, firstFunctionVariable is not defined
}
嵌套范围
当一个函数在另一个函数中定义时,内部函数可以访问外部函数的变量。 这种行为被称为词法作用域。
但是,外部函数无法访问内部函数的变量。
function outerFunction () {
const outer = `I'm the outer function!`
function innerFunction() {
const inner = `I'm the inner function!`
console.log(outer) // I'm the outer function!
}
console.log(inner) // Error, inner is not defined
}
为了直观地了解范围的工作原理,您可以想象单向玻璃。 您可以看到外面,但外面的人看不到您。

如果您的范围在范围内,则可视化多层单向玻璃。

在了解了目前关于范围的所有内容之后,您已经准备好弄清楚闭包是什么了。
闭包
每当您在另一个函数中创建函数时,您就创建了一个闭包。 内部函数是闭包。 此闭包通常会被返回,以便您可以在以后使用外部函数的变量。
function outerFunction () {
const outer = `I see the outer variable!`
function innerFunction() {
console.log(outer)
}
return innerFunction
}
outerFunction()() // I see the outer variable!
由于内部函数被返回,因此您也可以通过在声明函数时编写 return 语句来简化代码。
function outerFunction () {
const outer = `I see the outer variable!`
return function innerFunction() {
console.log(outer)
}
}
outerFunction()() // I see the outer variable!
由于闭包可以访问外部函数中的变量,因此它们通常用于两件事
- 控制副作用
- 创建私有变量
使用闭包控制副作用
副作用是指您除了从函数返回一个值之外,还执行了其他操作。 许多事情可能是副作用,例如 Ajax 请求、超时甚至console.log
语句
function (x) {
console.log('A console.log is a side effect!')
}
当您使用闭包控制副作用时,您通常会关注可能会破坏代码流程的副作用,例如 Ajax 或超时。
让我们通过一个示例来解释清楚。
假设您想为朋友的生日做蛋糕。 做蛋糕需要一秒钟,因此您编写了一个函数,该函数在一秒钟后记录made a cake
。
我在这里使用 ES6 箭头函数 来简化示例,使其更容易理解。
function makeCake() {
setTimeout(_ => console.log(`Made a cake`), 1000)
}
如您所见,此蛋糕制作函数有一个副作用:超时。
假设您还想让朋友选择蛋糕的口味。 为此,您可以在makeCake
函数中添加一个口味。
function makeCake(flavor) {
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
}
当您运行此函数时,请注意蛋糕在一秒钟后立即制作完成。
makeCake('banana')
// Made a banana cake!
这里的问题是您不想在知道口味后立即制作蛋糕。 您想等到合适的时候再制作。
要解决此问题,您可以编写一个prepareCake
函数来存储您的口味。 然后,在prepareCake
中返回makeCake
闭包。
从现在开始,您可以随时调用返回的函数,蛋糕将在 1 秒内制作完成。
function prepareCake (flavor) {
return function () {
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
}
}
const makeCakeLater = prepareCake('banana')
// And later in your code...
makeCakeLater()
// Made a banana cake!
这就是闭包用于减少副作用的方式 - 您创建一个函数,该函数可以根据您的意愿激活内部闭包。
闭包中的私有变量
如你所知,在函数中创建的变量无法在函数外部访问。由于无法访问,它们也被称为私有变量。
但是,有时你需要访问这样的私有变量。你可以借助闭包来实现。
function secret (secretCode) {
return {
saySecretCode () {
console.log(secretCode)
}
}
}
const theSecret = secret('CSS Tricks is amazing')
theSecret.saySecretCode()
// 'CSS Tricks is amazing'
上面的例子中,saySecretCode
是唯一一个将 secretCode
暴露在原始 secret 函数外部的函数(闭包)。因此,它也被称为**特权函数**。
使用 DevTools 调试作用域
Chrome 和 Firefox 的 DevTools 使你能够轻松地调试当前作用域内可访问的变量。有两种方法可以使用此功能。
第一种方法是在代码中添加 debugger
关键字。这将导致浏览器中的 JavaScript 执行暂停,以便你进行调试。
以下是一个使用 prepareCake
的示例
function prepareCake (flavor) {
// Adding debugger
debugger
return function () {
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
}
}
const makeCakeLater = prepareCake('banana')
如果你打开 DevTools 并导航到 Chrome 中的 Sources 选项卡(或 Firefox 中的 Debugger 选项卡),你将看到可供你使用的变量。

你也可以将 debugger
关键字移到闭包中。注意这次作用域变量是如何变化的
function prepareCake (flavor) {
return function () {
// Adding debugger
debugger
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
}
}
const makeCakeLater = prepareCake('banana')

第二种方法是直接在 sources(或 debugger)选项卡中点击行号,在代码中添加断点。

总结
作用域和闭包并不难理解。一旦你学会了如何通过单向玻璃来观察它们,它们就变得非常简单。
当你在函数中声明一个变量时,你只能在函数中访问它。这些变量被称为函数作用域。
如果你在另一个函数中定义任何内部函数,这个内部函数被称为闭包。它保留了对在外部函数中创建的变量的访问权限。
欢迎随时提出任何问题。我会尽快回复你。
如果你喜欢这篇文章,你可能也会喜欢我在我的博客和新闻简报上写的其他与前端相关的文章。我还有一门全新的(免费!)电子邮件课程:JavaScript 路线图。
写得不错,谢谢。但是,我认为
const
用于定义常量,而不是变量。我已经读过十多篇文章试图解释闭包,但这是第一篇让我明白的!非常感谢!这是我读过的最好的文章!
更正,以下代码
应该是
好眼力
没错,而且在所有的重复中都是这样 ;-)
否则,单向玻璃的想法很简单,而且非常准确。
这是一篇很棒的文章!
这就是我最喜欢的 css-tricks.com 上的文章类型:结构合理,对复杂主题的解释透彻。你总是会反复阅读这些文章。
你不会相信 flexbox 文章已经救了我多少次了。:D
但是,我有一个小小的批评:在你的文章中,听起来你应该避免在全局作用域中使用
const
和let
声明变量 - 尽管你应该尽量避免使用全局变量(正如你所写的那样),但在某些情况下你确实需要它们 - 而且在大多数情况下你应该使用常量(如果可能的话)或使用let
声明变量。几乎没有你想要变量遮蔽的情况 - 而且收到异常通常比代码中出现静默错误更好。写得非常好。烘焙蛋糕的例子让我有点迷茫。你能举一个真实的例子说明何时使用闭包来控制副作用吗?
我同意 Jacob 的观点。烘焙蛋糕的例子似乎造成副作用问题,因为配置和执行这两个关注点结合在一起。当然还有其他方法可以将这些步骤分开,因此我不清楚为什么要使用闭包来实现。
单向玻璃是我见过的关于闭包最好的类比。谢谢!
好文章。但我认为你建议不要使用提升函数是不正确的。提升可以使你的文件更易读,更清晰,因为你可以将复杂的函数抽象化并放在文件的末尾,以便在需要时进行检查。
使用好的编辑器,只需点击即可跳转到声明,并将核心逻辑放在文件的顶部,复杂的抽象化距离你只有一次点击。
我认为常识和代码风格指南应该是这种情况下主要的决策机制。
非常感谢。这是一篇关于作用域和闭包的很好的文章。
解释得很好。但这只是闭包和作用域的顶层概述。
如果你真的想知道闭包是如何工作的,以及它们是如何从父函数中访问变量和参数的,那么请访问这个很棒的博客
http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/
http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/
我知道这个博客上的内容很旧,但它是一笔宝贵的财富。我建议你从第 1 章开始阅读,因为所有概念都是相互关联的。
好文章,但你的嵌套作用域示例中有一个错误。你需要调用 innerFunction 才能按预期输出。
虽然 javascript 不介意在任何代码行的末尾省略分号字符,但我强烈建议大家这样做。我认为这样做没有任何害处,同时你也对代码行的结尾有了清晰的认识。在 javascript 中,省略分号字符有时会造成混乱。
很棒的文章
我也喜欢单向玻璃的例子,并在我的闭包文章中提到了它,重点是 for 循环。它只有 4 分钟的阅读量……看看吧
https://medium.com/@MoeHimed/closures-in-for-loops-in-layman-terms-1cf483e654bf