[译]JavaScript作用域和闭包

476 阅读8分钟

原文链接:JavaScript Scope and Closures

css-tricks.com/javascript-…

这篇文章主要讲解作用域的分类以及闭包的作用。文中所说通过“单面镜”理解作用域,还是挺形象的! 但译者对文中所提及的“副作用”并不是十分理解。对于JavaScript中的纯函数还需要进一步学习。

JavaScript作用域和闭包

作用域和闭包这两个概念在JavaScript中十分的重要。但是,在刚开始学习的时候它们令我十分的疑惑。这些有些关于作用域和闭包的解释去帮助你理解它们是什么。

让我们从“作用域”开始吧。

作用域

在JavaScript中作用域定义了你有权访问哪些便利。作用域一共分为两种 —— 分别是全局作用域和局部作用域。

全局作用域

如果一个变量在所有函数或者花括号之外声明,它则被定义为全局作用域

HEY!

这只是针对在web浏览器中使用JavaScript。你在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'

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

局部作用域

当变量在局部作用域中的特定部分被考虑和使用时,这些变量也被称之为“局部变量”。

####函数作用域

当你在函数内部定义了一个变量,你只能在函数内部使用这个变量。一旦你离开了这个函数就不能再获取到它了。

下面是一个例子,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

块级作用域是函数作用域的子集,因为函数声明时也需要一对花括号。(除了你在使用箭头函数时包含了隐性return)。

Function hoisting and scopes

当使用functiom函数声明时,它们总会被提升到当前作用域的最顶部。所以,以下两种情况是等同的:

// 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!

因为内部函数会被返回,所以你可以在声明函数的同时进行返回,从而缩短代码。

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或者超时。

让我们通过一个例子去理清这些关系。

想象一下,你正打算为你朋友的生日制作一个蛋糕。这个蛋糕需要花费一点时间,所以你编写了一个函数:在1s之后制作一个蛋糕。

function makeCake() {
  setTimeout(_ => console.log(`Made a cake`), 1000)
}

正如你看到的,这个制作蛋糕的函数会产生副作用:定时器。

再想象一下,你希望你的朋友可以为这个蛋糕选择一个口味。为了实现这个效果,你为makeCake这个函数新增了一个变量。

function makeCake(flavor) {
  setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000)
}

当你执行这个函数时,请注意这个蛋糕1s后就会被制作出来。

makeCake('banana')
// Made a banana cake!

这里的问题是你不希望在知道口味之后蛋糕马上就被制作出来。你希望在之后时机合适时它才被制作出来。

为了解决这个问题,你可以编写一个函数prepareCake去存储口味。然后,在prepare函数内部返回makeCake

从此以后,你可以在你想要的时候调用这个函数,然后蛋糕就会在1s后被制作出来。

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执行暂停,方便调试。

下面是preapreCake的例子:

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),你可以看到当前哪些变量可以使用。

你也可以移动debugger关键字到闭包中。请注意这时候的作用域变量发生了什么变量:

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

const makeCakeLater = prepareCake('banana')

使用此方法的第二种办法是:通过单击行好,直接在源代码中添加断点。

总结

作用域和闭包并不是很难理解,一旦你通过“单面镜”去理解它们就会变得十分简单。

当你在函数内部声明一个变量时,你只能在函数中访问它们。这些变量的作用域只在函数内部。

如果你在函数内部定义任何函数,这些内部函数就被称为为“闭包”。它可以继续访问外部函数定义的变量。

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


如果你有任何问题或意见,请给我留言!