开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
js中的作用域分为全局作用域和函数作用域两种。它们的作用都是规定变量和函数可访问范围的规则。
全局作用域
全局作用域中定义的变量和函数可以在程序的任何地方被访问到。一般想要定义全局作用域,可以从以下三个定义中选取。
window下的变量和方法
window.name
window.location
window.age
最外层声明的变量和方法
const a = 1
function fn(){}
const arr = [4,5,6]
非严格模式下,函数作用域中未定义但直接赋值的变量和方法
非严格模式下,这样的变量自动变为window下挂载的属性,因此他们也属于全局作用域。
function fn(){
a = 30
}
function fn1() {
return a + 30
}
fn1() // 60
在实践中,我们应该尽量少的去定义全局的变量或方法,主要是以下几个原因:
- 我们可能会无意间修改全局变量的值,但是其他的地方不知道,
- 不同开发者在同一个项目中,如果都使用全局变量的话,很容易命名冲突
- 应用程序执行过程中,全局变量无法释放
全局变量声明的时候,有一个需要关注的细节。我们使用var声明的变量,会被挂载到全局对象window之下。而被let/const声明的变量,不会修改window的对象,而是会被挂载到一个新的对象Script之下
var a = 10
console.log(window.a) // 10
const b = 20
console.log(window.b) // undefined
函数作用域
每一个花括号 {} 都可以代表一个代码块。但是需要注意的是,不是所有的花括号都能具备自己的作用域。函数声明或者函数表达式,都能让花括号具备函数作用域,这样的我们就叫做函数作用域。函数作用域中声明的变量和方法,只能被该函数下级的子层作用域访问到,不能被其他不相关的作用域访问。
function fn(){
const a = 20
const b = 30
}
fn()
function bar() {
return a + b // 这里无法访问fn中的变量
}
function fn() {
const a = 20
const b = 30
function bar() {
return a + b // 子作用域,可以访问到
}
return bar
}
fn()
那么还有一个问题,作用域的信息是什么时间点产生的呢?答案其实是在函数定义时就已经确定了,因此在代码执行前,就已经明确了作用域的范围,如下:
function fn() {
const a = 10
const b = 20
function bar() {
return a
}
console.dir(bar)
}
fn()
例子中,我们定义了fn和它的内部函数bar,执行fn时,可以看到bar并没有在fn中执行,只是进行了定义,但是我们可以在打印中看到,bar的相关作用域信息已经生成了。
ƒ bar()
arguments: null
caller: null
length: 0
name: "bar"
prototype: {constructor: ƒ}
[[FunctionLocation]]: VM206:5
[[Prototype]]: ƒ ()
[[Scopes]]: Scopes[2]
0: Closure (fn) {a: 10}
1: Global {window: Window, self: Window, ...}
其中的[[Scopes]]就是代表了该函数的作用域情况,我们可以看到,其中包含两个对象,Closure就是闭包fn,是bar的上一层作用域,Global是全局作用域,每个函数都会存在。
这里还有一个小细节点,我们在fn中声明了两个变量a,b,但是在bar的作用域中却只出现了a,这是因为在bar中只使用了a。
完整的作用域链
通过上面的学习,我们知道每个函数声明后都具备一个属性[[Scopes]],该属性中存储当前的函数可以访问的所有变量和方法,这些变量和方法有以下几个分类:
- Global对象: 包含全局对象的所有属性方法
- Script对象:在全局环境下由let和const声明的变量对象
- Closure对象:嵌套函数生成,仅会保存当前作用域能够访问的变量属性
- Local对象:上面的变量对象,都会保存在[[Scopes]]属性中,因为他们在函数定义时就确定好了,而Local对象实在函数执行的过程中才会确定下来,并且在执行时他的属性还会随时发生变化,