本文详细解析JavaScript中的全局与局部作用域、函数与块级作用域的区别,深入探讨闭包的原理及其在减少副作用和创建私有变量中的应用。通过双向镜比喻帮助理解词法作用域,并提供使用debugger和断点调试作用域的实际操作指南。
JavaScript作用域与闭包概述
作用域和闭包是JavaScript中至关重要的概念。对于初学者而言,理解它们往往存在难度。本文将通过具体示例,帮助你清晰掌握这两个核心知识点。
我们将首先从作用域的基本概念入手。
理解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中存在两种局部作用域:函数作用域和块级作用域。
我们从函数作用域开始讲解。
函数作用域详解
函数作用域
当在函数内部定义一个变量时,该变量在函数体内的任何位置都可以被访问。然而,在函数外部,将无法访问该变量。
以下示例展示了在sayHello函数内部的hello变量:
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关键字定义的函数会被提升到当前作用域的顶部。因此,以下代码是等效的:
// 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!
由于闭包可以访问外层函数的变量,因此它们通常有两种用途:
- 减少副作用
- 创建私有变量
利用闭包控制副作用
使用闭包控制副作用
当你在函数返回值时执行某些操作,通常会发生一些副作用。副作用在很多情况下都会发生,比如Ajax调用、超时处理,或者哪怕是console.log的输出语句:
function (x) {
console.log('A console.log is a side effect!')
}
当你使用闭包来控制副作用时,实际上需要考虑哪些可能会混淆代码工作流程的部分,比如Ajax或超时。
要把事情说清楚,还是看例子比较方便:
比如说你要为朋友庆生,做一个蛋糕。做这个蛋糕可能花1秒钟的时间,所以你写了一个函数记录在一秒钟以后,记录做完蛋糕这件事。
为了让代码简短易读,我使用了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。
从这里开始,你就可以在你需要的时候调用,蛋糕也会在一秒后立刻做好。
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这一变量。因此,它也被成为特权函数。
使用开发者工具调试作用域
使用DevTools调试
Chrome和Firefox的开发者工具都使我们能很方便的调试在当前作用域内可以访问的各种变量,一般有两种方法。
第一种方法是在代码里使用debugger关键词。这能让浏览器里运行的JavaScript暂停,以便调试。
下面是prepareCake的例子:
function prepareCake (flavor) {
// Adding debugger
debugger
return function () {
setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}
}
const makeCakeLater = prepareCake('banana')
打开Chrome的开发者工具,定位到Source页下(或者是Firefox的Debugger页),你就能看到可以访问的变量了。

使用debugger调试prepareCake的作用域。
你也可以把debugger关键词放在闭包内部。注意对比变量的作用域:
function prepareCake (flavor) {
return function () {
// Adding debugger
debugger
setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
}
}
const makeCakeLater = prepareCake('banana')

调试闭包内部作用域
第二种方式是直接在代码相应位置加断点,点击对应的行数就可以了。

通过断点调试作用域
总结一下
闭包和作用域并不是那么难懂。一旦你使用双向镜的思维去理解,它们就非常简单了。
当你在函数里声明一个变量时,你只能在函数内访问。这些变量的作用域就被限制在函数里了。
如果你在一个函数内又定义了内部函数,那么这个内部函数就被称作闭包。它仍可以访问外部函数的作用域。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。