作用域链

作用域定义

  • 作用域是指在程序源代码中 定义变量的区域
  • 作用域规定了 如何查找变量 ,也就是确定当前执行代码对变量的访问权限。
  • 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)

如上,abc 以及 anbn 都在全局作用域中,而全局作用域中声明的变量和函数会作为 window 对象的属性和方法保存。

TIP

  • 在函数作用域中,不使用变量关键字声明的变量,在赋值时会往上一级作用域寻找已经声明的同名变量,若直到全局作用域时还没找到,则会成为全局对象 window 的属性。

  • 在没有块级作用域的情况下,forif 等代码块中声明的变量也属于全局变量。

    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 的内部变量,所以在外部访问会报错。

块级作用域

包含 letconst 关键字声明的一对 {} 内的词法作用域。

{
    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
]

参考


  1. 可视为执行上下文中的变量对象 ↩︎

Last Updated:
Contributors: Vsnoy