作用域

MDN中的引用

当前的执行上下文。值和表达式在其中 "可见" 或可被访问到的上下文。如果一个变量或者其他表达式不 "在当前的作用域中",那么它就是不可用的。 作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。

作用域说的直白点就是规定了如何查找变量(标识符),当前执行环境(执行上下文)对变量的访问权限

JavaScript 采用词法作用域(静态作用域)

词法作用域

下面是维基百科的解释

静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。

相反,采用动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。这意味着如果有个函数f,里面调用了函数g,那么在执行g的时候,f里的所有局部变量都会被g访问到。而在静态作用域的情况下,g不能访问f的变量。动态作用域里,取变量的值时,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。显然,最外层的绑定即是全局状态下的那个值。

词法字面意思应该就是定义的时候,比如一个函数在定义(编写)时候就已经确定了作用域关系,相反动态作用域是在函数调用的时候确定作用域

来个栗子

var a = 1function foo() {  var a = 2
   return function f(){       console.log(a)
  } 
}var f = foo()
f() // 2复制代码

f 函数因为是在 foo 函数中定义(编写)的,根据词法作用域的定义,f 函数不管在哪里调用访问到的 a 变量都是创建 f 函数 那个函数(foo)作用域中的,后面要讲到作用域链也是通过(词法作用域)一层层向上查找变量(标识符)。

假设上的代码,如果是动态作用域 f 函数打印应该是1,函数调用是访问全局环境下的变量 a 。(动态作用域我也是一知半解,不对的地方欢迎指正)

作用域链

那什么是作用域链了,上面有提到,查找变量(标识符)是一层层向父级(词法层面的父级)作用域上查找,准确点说应该是去父级执行环境(执行上下文)变量对象中查找。

前面有说过函数是在定义时就确定了作用域,那是因为函数有一个内部属性 [[scope]],如果把 scope 看做是有个数组,当函数创建时就会把父级的变量对象添加到其中。

来个栗子

function f1() {	function f2() {
   	...
    }
}复制代码

创建时各自的 [[scope]]

f1.[[scope]] = [
 globalContext.VO
]

f2.[[scope]] = [
  f1Context.AO,
  globalContext.VO
]复制代码

变量(标识符)查找就是通过 [[scope]] ,需要注意的是这里的作用域链是建立在词法层面的,这里父级变量对象是词法层面的父级。

执行上下文栈

开始之前先聊下执行环境(执行上下文)

执行上下文

执行上下文的三个总要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

全局执行上下文,根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样。在 Web 浏览器中,全局执行上下文变量对象被认为是 window 对象

函数执行上下文,每个函数都有自己的执行上下文,当执行流进入一个函数时,函数的执行上下文就会被推入一个执行上下文栈中。函数执行上下文将其活动对象表示变量对象

  • 变量对象是与执行上下文相关的数据作用域,存储了在上下文(执行环境)中定义的变量和函数声明。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值
  • 作用域链看上节的分析

执行上下文栈

执行上下文栈是用来管理执行上下文的,代码中我们会创建很多的函数,在函数执行时都会创建对应的函数执行上下文,这些执行上下文该如何管理全听命与执行上下文栈。

一般 JavaScript 开始要解释执行代码的时候全局执行上下文会首先压入栈中,在是我们自己的定义的函数创建的执行上下文会依次压入栈中,函数执行上下文执行完毕会先进后出的原则依次弹栈,执行栈底部永远会保留一个全局执行上下文,直到程序结束。

总结

JavaScirpt 中应用是词法作用域 ,及作用域链是根据词法层面的父级关系来生成完整变量(标识符)的访问权限,每个函数都有个存储作用域链的属性[[scope]],在执行函数时会创建对应的函数执行上下文,执行上下文会压入执行栈中,压入栈后执行上下文会做些初始化

  1. 复制函数 [[scope]] 属性创建作用域链,
  2. 用 arguments 创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入作用域链顶端。

作用域(链)负责变量访问规则和权限,提供给执行上下文(执行环境),执行上下文执行时按照作用域提供的规则来访问它需要的变量