你不可不知道的 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'
所以,你应该使用局部变量,而不是全局变量。
局部作用域
在你代码特定范围之内申明的变量可以称为处于局部作用域中,这些变量也被称为局部作用域。
在 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
}
这个图片介绍了它是如何工作的,你能够想象一面单面镜。你能够看见外面的人,外面的人却无法看见你。
嵌套作用域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!
因为闭包允许变量来自外部的函数,他们通常被用来
- 控制副作用
- 创建私有变量
用闭包控制副作用
当你从一个函数中返回一个值时会产生副作用。很多事情都会有副作用,比如 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。