JavaScript 范围和闭包

Avatar of Zell Liew
Zell Liew

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

范围和闭包在 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!'

虽然您可以在全局范围中声明变量,但建议不要这样做。 这是因为有命名冲突的可能性,即两个或多个变量具有相同的名称。 如果您使用constlet声明变量,则每当发生命名冲突时,您都会收到错误。 这是不可取的。

// 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 中,有两种局部范围:函数范围和块范围。

让我们先谈谈函数范围。

函数范围

当您在函数中声明一个变量时,您只能在函数内访问它。 一旦您退出函数,您就无法访问它。

在下面的示例中,变量hellosayHello范围内

function sayHello () {
  const hello = 'Hello CSS-Tricks Reader!'
  console.log(hello)
}

sayHello() // 'Hello CSS-Tricks Reader!'
console.log(hello) // Error, hello is not defined

块范围

当您使用constlet在花括号 ({}) 内声明变量时,您只能在该花括号内访问它。

在下面的示例中,您可以看到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!

由于闭包可以访问外部函数中的变量,因此它们通常用于两件事

  1. 控制副作用
  2. 创建私有变量

使用闭包控制副作用

副作用是指您除了从函数返回一个值之外,还执行了其他操作。 许多事情可能是副作用,例如 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 选项卡),你将看到可供你使用的变量。

调试 prepareCake 的作用域

你也可以将 debugger 关键字移到闭包中。注意这次作用域变量是如何变化的

function prepareCake (flavor) {
  return function () {
    // Adding debugger
    debugger
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
  }
}

const makeCakeLater = prepareCake('banana')
调试闭包作用域

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

通过添加断点来调试作用域

总结

作用域和闭包并不难理解。一旦你学会了如何通过单向玻璃来观察它们,它们就变得非常简单。

当你在函数中声明一个变量时,你只能在函数中访问它。这些变量被称为函数作用域。

如果你在另一个函数中定义任何内部函数,这个内部函数被称为闭包。它保留了对在外部函数中创建的变量的访问权限。

欢迎随时提出任何问题。我会尽快回复你。

如果你喜欢这篇文章,你可能也会喜欢我在我的博客新闻简报上写的其他与前端相关的文章。我还有一门全新的(免费!)电子邮件课程:JavaScript 路线图