你不可不知道的 JavaScript 作用域和闭包

原文出处:JavaScript Scope and Closures

作用域和闭包是 JavaScript 中重要的部分,但是当我开始学习时遇到了很多的困惑。这里就是一篇关于作用域和闭包的文章,能够帮助你理解它们。

让我们先从作用域开始

作用域

JavaScript 作用域指定了哪些变量你能够访问。有两种作用域 —— 全局作用域和局部作用域

全局作用域

如果一个变量在函数外面或者大括号({})外申明,那么就是定义了一个全局作用域的变量。

这个只是对于浏览器中的 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'

所以,你应该使用局部变量,而不是全局变量。

扫描二维码关注公众号,回复: 926337 查看本文章

局部作用域

在你代码特定范围之内申明的变量可以称为处于局部作用域中,这些变量也被称为局部作用域。

在 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

块作用域

当你在一个大括号中({})使用 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
}

这个图片介绍了它是如何工作的,你能够想象一面单面镜。你能够看见外面的人,外面的人却无法看见你。

嵌套作用域01

如果你遇见了嵌套作用域,可以理解成多层单面玻璃。

嵌套作用域01

当你彻底理解作用域之后,你才能够进一步理解闭包的原理。

闭包

当你在一个函数内部创建一个函数时,你就创建了一个闭包。内部函数就是闭包。这个闭包总是会 return 出来,所以你能够稍后使用外部函数中的变量。

function outerFunction () {
  const outer = `I see the outer variable!`

  function innerFunction() {
    console.log(outer)
  }

  return innerFunction
}

outerFunction()() // I see the outer variable!

当内部函数需要 return 时,你可以直接 reutrn 函数声明,这样的代码更加的精练。

function outerFunction () {
  const outer = `I see the outer variable!`

  return function innerFunction() {
    console.log(outer)
  }
}

outerFunction()() // I see the outer variable!

因为闭包允许变量来自外部的函数,他们通常被用来

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

用闭包控制副作用

当你从一个函数中返回一个值时会产生副作用。很多事情都会有副作用,比如 Ajax 请求,timeout 或者一个 console.log。

function (x) {
  console.log('A console.log is a side effect!')
}

当你使用闭包来解决副作用时,你通常会关心这样弄乱你代码,像 Ajax 或者 timeouts。

让我们通过一个例子来理清这些。

比如你想要为你给你朋友的生日制作一个蛋糕。这个蛋糕需要一秒钟制作完成,所以你写了一个函数,在一秒后打印出 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 中 return makeCake 函数。

使用这个方法,你能够在任何时候调用 return 的函数,蛋糕会在一秒钟之后制作。

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 解释器遇见 debugger 会在浏览器中暂停,这样你就能够调试。

这里是一个 prepareCake 的例子:

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

const makeCakeLater = prepareCake('banana')

如果你在 Chrome 中打开 DevTools 然后找到 Sources 选项(或者 Firefox 中的 Debugger 选项),你就能看到可用的变量。

使用 DevTools 调试作用域01

你也能把 debugger 放在闭包中,注意这个时候局部变量是怎么变化的。

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

const makeCakeLater = prepareCake('banana')

使用 DevTools 调试作用域02

第二种方式是在 sources (或者 debugger) 选项中点击行数使用 debugging 功能来直接在你的代码中添加断点。

使用 DevTools 调试作用域03

结束语

作用域和闭包并不是非常的难理解。一旦你真正理解了其中的原理,他们就变得非常的简单了。

当你在函数中声明一个变量,你只能够在该函数中使用它。这些变量就被限制在了函数范围之内。

如果你在其他函数中定义一个内部函数,这个内部函数被称之为闭包。它保留了在外部函数中声明的变量。

期待你提出问题,我将尽我所能的回复你。

如果你喜欢这篇文章,你也许也喜欢我写的其他文章,可以访问我的博客newsletter。我也一个免费的课程:JavaScript Roadmap

猜你喜欢

转载自blog.csdn.net/xjlinme/article/details/77720321