JavaScript中的作用域和闭包
作用域和闭包在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
关键词来声明变量,那么当你遇到命名冲突时,将会报错。这样做是不可取的。
// 不要这样做
let thing = 'something'
let thing = 'something else' // => Error, thing has already been declared
如果你使用的是var
关键词来声明变量,那么第二个变量将会覆盖第一个变量(变量名相同的情况之下)。这样也是不可取的,因为这样做会加大你调试代码的难度。
// 不要这样做
var thing = 'something'
var thing = 'something else' // 也许在你的代码中是在不同的地方
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
块作用域是函数作用域的子集。因为函数需要用大括号来声明(除了ES6中的箭头函数)。
函数提升和作用域
函数在声明为函数时,总是被提升到当前作用域的顶部。所以这两个是等价的。
// 这和下面的是一样的
sayHello() // => Hello CSS-Tricks Reader!
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
// 这和上面的是一样的
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
sayHello() // => Hello CSS-Tricks Reader!
当使用函数表达式声明函数时,函数不会被提升到当用作用域的顶部。
sayHello() // => Error, sayHello is not defined
const sayHello = function () {
console.log(aFunction)
}
由于这两种变化,函数提升可能会令人困惑,不应该被使用。在使用函数之前,最好先声明你要用的函数。
函数不能访问彼此的作用域
当你单独定义函数时,函数不能访问彼此的作用域。即使一个函数可以在另一个函数中使用。
在下面的这个例子中,second()
函数不能访问first()
函数中的firstFunctionVariable
变量。
function first () {
const firstFunctionVariable = `I'm part of first`
}
function second () {
first()
console.log(firstFunctionVariable) // => Error, firstFunctionVariable is not defined
}
作用域嵌套
当函数在另一个函数中定义时,内部函数可以访问外部函数的变量。这种行为称为词法作用域(Lexical Scoping)。
然而,外部函数无法访问内部函数的变量。
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
}
为了可视化作用域如何工作,你可以想象单面玻璃(one-way glass)。你可以看到外面,但外面的人看不到你。
如果你在作用域内有作用域,多个层次的单向玻璃看起来像这样:
只有在了解了关于作用域的所有内容之后,你才能更好的了解闭包是什么。
闭包
当你在另一个函数中创建函数时,你已经创建了一个闭包。内部函数就是一个闭包。这个闭包常会返回内部函数,这样你就可以使用外部函数的变量。
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!
由于闭包可以访问外部函数中的变量,所以它们通常用于两种情况:
- 控制副作用(side effects)
- 创建私有变量
使用闭包控制副作用
当你在不从函数返回值的情况下执行某些操作时,会发生副作用。很多东西都可能会产生副作用,比如Ajax请求,timeout
,甚至是console.log
语句。
function (x) {
console.log('A console.log is a side effect!')
}
当你使用闭包来控制副作用时,你通常会关心那些可能会打乱你的代码流,比如Ajax或timeout
。
让我们通过一个例子来让事情更清楚些。
假设你想为你朋友的生日做一个蛋糕。这个蛋糕需要一秒钟的时间,所以你写了一个函数,在一秒钟打印出made a cake
。
下面的示例使用ES6的箭头函数,使示例变得更短,更容易理解。
function makeCake() {
setTimeout(_ => console.log(`Made a cake`, 1000)) // => Made a cake 1000
}
正如你所见,这个制作蛋糕的函数有一个副作用:超时。
让我们再深入一点,你想让你的朋友为蛋糕选择一种口味。为此,你可以为你的makeCake()
函数添加一个味道(flavor
):
function makeCake(flavor) {
setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}
当你运行这个函数时,注意,蛋糕在一秒钟后就会被制作出来。
makeCake('banana') // => Made a banana cake!
这里的问题是,你不希望在知道味道后立即做蛋糕。你想在时机成熟的时候再做这个蛋糕。
为了解决这个问题,你可以编写一个prepareCake()
的函数来存储你需要的蛋糕味道。然后在prepareCake()
中返回makeCake()
,形成一个闭包。
function prepareCake (flavor) {
return function () {
setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}
}
const makeCakeLater = prepareCake('banana')
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并选择Sources
选项,你就会看到可用的变量。
你还可以将debugger
放到闭包内。注意作用域中变量的变化:
function prepareCake (flavor) {
return function () {
// Adding debugger
debugger
setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}
}
const makeCakeLater = prepareCake('banana')
第二种方法通过打断点来调式。
总结
作用域和闭包并不难以理解。一旦你知道如何通过向单玻璃看到它们,它们就很简单了。
当你在函数中声明变量时,你只能在函数中访问它。这些称为函数作用域。
如果在一个函数内定义任何函数,这个内部函数称为闭包。它保留对在外部函数中创建的变量的访问权。
如果有任何问题,欢迎在下面的评论中一起讨论。
本文根据@ZELL LIEW的《JavaScript Scope and Closures》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/javascript-scope-closures/。
如需转载,烦请注明出处:https://www.fedev.cn/javascript/javascript-scope-closures.htmlnike air max 1 zappos