作用域
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 对象。
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
- 作用域链看上节的分析
执行上下文栈
执行上下文栈是用来管理执行上下文的,代码中我们会创建很多的函数,在函数执行时都会创建对应的函数执行上下文,这些执行上下文该如何管理全听命与执行上下文栈。
一般 JavaScript 开始要解释执行代码的时候全局执行上下文会首先压入栈中,在是我们自己的定义的函数创建的执行上下文会依次压入栈中,函数执行上下文执行完毕会先进后出的原则依次弹栈,执行栈底部永远会保留一个全局执行上下文,直到程序结束。
总结
JavaScirpt 中应用是词法作用域 ,及作用域链是根据词法层面的父级关系来生成完整变量(标识符)的访问权限,每个函数都有个存储作用域链的属性[[scope]],在执行函数时会创建对应的函数执行上下文,执行上下文会压入执行栈中,压入栈后执行上下文会做些初始化
- 复制函数 [[scope]] 属性创建作用域链,
- 用 arguments 创建活动对象,
- 初始化活动对象,即加入形参、函数声明、变量声明,
- 将活动对象压入作用域链顶端。
作用域(链)负责变量访问规则和权限,提供给执行上下文(执行环境),执行上下文执行时按照作用域提供的规则来访问它需要的变量