浅谈JavaScript作用域和闭包
前端之家收集整理的这篇文章主要介绍了
浅谈JavaScript作用域和闭包,
前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
@H_403_0@作用域和闭包在JavaScript里非常重要。但是在我最初学习JavaScript的时候,却很难理解。这篇文章会用一些例子帮你理解它们。
@H_
4030@我们先从作用域开始。
@H4030@作用域
@H4030@JavaScript的作用域限定了你可以访问哪些变量。有两种作用域:全局作用域,局部作用域。
@H4030@全局作用域
@H403_0@在所有
函数声明或者大括号之外定义的变量,都在全局作用域里。
@H_
403_0@不过这个规则只在浏览器中运行的JavaScript里有效。如果你在Node.js里,那么全局作用域里的变量就不一样了,不过这篇
文章不讨论Node.js。
<div class="jb51code">
<pre class="brush:js;">
const globalVariable = 'some value'
@H_
403_0@一旦你声明了一个
全局变量,那么你在任何地方都可以使用它,
包括函数内部。
function sayHello () {
console.log(hello)
}
console.log(hello) // 'Hello css-Tricks Reader!'
sayHello() // 'Hello css-Tricks Reader!'
@H_
403_0@尽管你可以在全局作用域定义变量,但我们并不推荐这样做。因为可能会引起命名冲突,两个或更多的变量使用相同的变量名。如果你在定义变量时使用了
const
或者
let
,那么在命名有冲突时,你就会收到
错误提示。这是不可取的。
@H_
403_0@如果你定义变量时使用的是
var
,那第二次定义会覆盖第一次定义。这也会让
代码更难调试,也是不可取的。
@H_
403_0@所以,你应该尽量使用局部变量,而不是
全局变量
@H_
403_0@
局部作用域
@H_
403_0@在你
代码某一个具体范围内使用的变量都可以在局部作用域内定义。这就是局部变量。
@H_
403_0@JavaScript里有两种局部作用域:
函数作用域和块级作用域。
@H_
403_0@我们从
函数作用域开始。
@H_
403_0@
@H_
403_0@当你在
函数里定义一个变量时,它在
函数内任何地方都可以使用。在
函数之外,你就无法访问它了。
@H_
403_0@比如下面这个例子,在
sayHello
函数内的
hello
变量:
sayHello() // 'Hello
css-Tricks Reader!'
console.log(hello) // Error,hello is not defined
@H_
403_0@
块级作用域
@H_
403_0@你在使用大括号时,声明了一个
const
或者
let
的变量时,你就只能在大括号内部使用这一变量。
@H_
403_0@在下例中,
hello
只能在大括号内使用。
css-Tricks Reader!'
}
console.log(hello) // Error,hello is not defined
@H_
403_0@块级作用域是
函数作用域的子集,因为
函数是需要用大括号定义的,(除非你明确使用return语句和箭头
函数)。
@H_
403_0@
函数提升和作用域
@H_
403_0@当使用function定义时,这个
函数都会被提升到当前作用域的顶部。因此,下面的
代码是等效的:
css-Tricks Reader!')
}
// This is the same as the code above
function sayHello () {
console.log('Hello css-Tricks Reader!')
}
sayHello()
@H_
403_0@使用
函数表达式定义时,
函数就不会被提升到变量作用域的顶部。
@H_
403_0@因为这里有两个变量,
函数提升可能会导致混乱,因此就不会生效。所以一定要在使用
函数之前定义
函数。
@H_
403_0@
函数不能访问其他函数的作用域
@H_
403_0@在分别定义的不同的
函数时,虽然可以在一个
函数里
调用一个
函数,但一个
函数依然不能访问其他
函数的作用域内部。
@H_
403_0@下面这例,
second
就不能访问
firstFunctionVariable
这一变量。
function second () {
first()
console.log(firstFunctionVariable) // Error,firstFunctionVariable is not defined
}
@H_
403_0@
嵌套作用域
@H_
403_0@如果在
函数内部又定义了
函数,那么内层
函数可以访问外层
函数的变量,但反过来则不行。这样的
效果就是词法作用域。
@H_
403_0@外层
函数并不能访问内部
函数的变量。
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
}
@H_
403_0@如果把作用域的机制可视化,你可以想象有一个双向镜(单面透视玻璃)。你能从里面看到外面,但是外面的人不能看到你。
@H_
403_0@

@H_
403_0@
函数作用域就像是双向镜一样。你可以从里面向外看,但是外面看不到你。
@H_
403_0@嵌套的作用域也是相似的机制,只是相当于有更多的双向镜。
@H_
403_0@

@H_
403_0@多层
函数就意味着多个双向镜。
@H_
403_0@理解前面关于作用域的部分,你就能理解闭包是什么了。
@H_
403_0@
闭包
@H_
403_0@你在一个
函数内新建另一个
函数时,就相当于创建了一个闭包。内层
函数就是闭包。通常情况下,为了能够使得外部
函数的内部变量可以访问,一般都会返回这个闭包。
function innerFunction() {
console.log(outer)
}
return innerFunction
}
outerFunction()() // I see the outer variable!
@H_
403_0@因为内部
函数是返回值,因此你可以简化
函数声明的部分:
return function innerFunction() {
console.log(outer)
}
}
outerFunction()() // I see the outer variable!
@H_
403_0@因为闭包可以访问外层
函数的变量,因此他们通常有两种用途:
- 减少副作用
- 创建私有变量
@H_
403_0@
使用闭包控制副作用
@H_
403_0@当你在
函数返回值时执行某些操作时,通常会发生一些副作用。副作用在很多情况下都会发生,比如Ajax
调用,超时处理,或者哪怕是
console.log
的
输出语句:
@H_
403_0@当你使用闭包来控制副作用时,你实际上是需要考虑哪些可能会混淆
代码工作流程的部分,比如Ajax或者超时。
@H_
403_0@要把事情说清楚,还是看例子比较方便:
@H_
403_0@比如说你要给为你朋友庆生,做一个蛋糕。做这个蛋糕可能花1秒钟的时间,所以你写了一个
函数记录在一秒钟以后,记录做完蛋糕这件事。
@H_
403_0@为了让
代码简短易读,我使用了ES6的箭头
函数:
console.log(`Made a cake`,1000)
)
}
@H_
403_0@如你所见,做蛋糕带来了一个副作用:一次延时。
@H_
403_0@更进一步,比如说你想让你的朋友能选择蛋糕的口味。那么你就给做蛋糕
makeCake
这个
函数加了一个参数。
console.log(`Made a ${flavor} cake!`,1000))
}
@H_
403_0@因此当你
调用这个
函数时,一秒后这个新口味的蛋糕就做好了。
@H_
403_0@但这里的问题是,你并不想立刻知道蛋糕的味道。你只需要知道时间到了,蛋糕做好了就行。
@H_
403_0@要
解决这个问题,你可以写一个
prepareCake
的
功能,保存蛋糕的口味。然后,在返回在内部
调用prepareCake
的闭包
makeCake
。
@H_
403_0@从这里开始,你就可以在你需要的时
调用,蛋糕也会在一秒后立刻做好。
console.log(`Made a ${flavor} cake!`,1000))
}
}
const makeCakeLater = prepareCake('banana')
// And later in your code...
makeCakeLater()
// Made a banana cake!
@H_
403_0@这就是使用闭包减少副作用:你可以创建一个任你驱使的内层闭包。
@H_
403_0@
私有变量和闭包
@H_
403_0@前面已经说过,
函数内的变量,在
函数外部是不能访问的既然不能访问,那么它们就可以称作私有变量。
@H_
403_0@然而,有时候你确实是需要访问私有变量的。这时候就需要闭包的帮助了。
const theSecret = secret('CSS Tricks is amazing')
theSecret.saySecretCode()
// 'CSS Tricks is amazing'
@H_
403_0@这个例子里的
saySecretCode
函数,就在原
函数外暴露了
secretCode
这一变量。因此,它也被成为特权
函数。
@H_
403_0@
使用DevTools调试
@H_
403_0@Chrome和Firefox的开发者工具都使我们能很方便的调试在当前作用域内可以访问的各种变量一般有两种
方法。
@H_
403_0@第一种
方法是在
代码里使用
debugger
关键词。这能让浏览器里运行的JavaScript的暂停,以便调试。
@H_
403_0@下面是
prepareCake
的例子:
console.log(`Made a ${flavor} cake!`,1000))
}
}
const makeCakeLater = prepareCake('banana')
@H_
403_0@打开Chrome的开发者工具,定位到Source页下(或者是Firefox的Debugger页),你就能看到可以访问的变量了。
@H_
403_0@

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

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

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