作用域链
作用域定义
- 作用域是指在程序源代码中 定义变量的区域 。
- 作用域规定了 如何查找变量 ,也就是确定当前执行代码对变量的访问权限。
JS采用的是 词法作用域 ,也就是静态作用域。函数作用域在函数 定义时 就决定了。
通俗地理解,作用域就是变量与函数的可访问范围,控制着变量和函数的可见性和生命周期。
TIP
为什么函数的作用域在函数定义的时候就决定了?
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
function foo() {
function bar() {
...
}
foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]
作用域分类
全局作用域
所有在函数外部声明的变量都处于全局作用域中。全局作用域中声明的变量和函数会作为全局对象 window 的属性和方法保存,可被任意访问,其生命周期伴随着页面的生命周期。
a = 10 // 可省略 var
var b = 20
function an(){
c = 3 // 函数作用域中未声明而直接赋值的变量,属于全局变量
console.log('an')
}
var bn = function(){
console.log('bn')
}
console.log(window)
如上,a、b、c 以及 an、bn 都在全局作用域中,而全局作用域中声明的变量和函数会作为 window 对象的属性和方法保存。
TIP
在函数作用域中,不使用变量关键字声明的变量,在赋值时会往上一级作用域寻找已经声明的同名变量,若直到全局作用域时还没找到,则会成为全局对象
window的属性。在没有块级作用域的情况下,
for、if等代码块中声明的变量也属于全局变量。for(var i = 0; i < 10; i++) { ... } console.log(i) // 10 if(true) { var a = 1 } console.log(a) // 1
函数作用域
函数声明内的词法作用域。函数内部定义的变量或函数只能在函数内部被访问。函数执行结束之后,函数内部定义的局部变量会被销毁(闭包除外)。
var a = 1
function foo(){
var b = 2
console.log(a) // 1
console.log(b) // 2
}
foo();
console.log(a) // 1
console.log(b) // Uncaught ReferenceError: b is not defined
如上,a 是全局变量,可被任意访问,而 b 是只属于函数 foo 的内部变量,所以在外部访问会报错。
块级作用域
包含 let 和 const 关键字声明的一对 {} 内的词法作用域。
{
console.log(a) // Uncaught ReferenceError: a is not defined
let a = 1
console.log(a) // 1
}
console.log(a) // Uncaught ReferenceError: a is not defined
这样,通过 let 声明的变量就只在所在代码块内有效,不会泄漏到全局,也不会如 var 声明的变量被提升到块的顶部。
作用域链
TIP
- 作用域 ≈ 变量对象(个人理解)
- 作用域链 = 变量对象 + 父环境作用域链
- 作用域链是由多个变量对象串连起来的一条链,整个作用域链构成了当前执行环境中变量和函数可访问的范围,即作用域。(个人理解)
作用域链本质上就是查找变量的链条。当访问一个变量时,解释器首先会在当前作用域 [1] 中查找, 若没找到则会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的全局作用域为止,这就是作用域链。
可用以下几句话来概括:
- 查看当前作用域,若当前作用域声明了这个变量,就确定结果
- 查看当前作用域的上级作用域中有没有声明
- 依次往上查找,直到全局作用域为止
- 若全局作用域中也没有,我们就认为这个变量未声明
思考题
结合着变量对象和执行上下文栈的知识,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程。
var scope = 'global scope'
function checkscope(){
var scope2 = 'local scope'
return scope2
}
checkscope()
执行过程如下:
checkscope 函数被创建,保存作用域链到函数内部属性 [[scope]]
checkscope.[[scope]] = [
globalContext.VO
]
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
]
checkscope 函数不是立刻开始执行,在真正开始执行之前,会先做一些准备工作
第一步:复制函数内部属性 [[scope]] 来构建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]]
}
第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
]
参考
可视为执行上下文中的变量对象 ↩︎