阅读 243

javascript作用域和执行上下文

什么是作用域?

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域

静态作用域 & 动态作用域?

  • 静态作用域: 函数的作用域在定义时就确定了
  • 动态作用域: 函数的作用域在函数调用时才确定

看一个例子:

var value = 1;

function foo() {
    console.log(value); // ?
}

function bar() {
    var value = 2;
    foo();
}

bar()

复制代码

假设从静态作用域的角度看这题:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设从动态作用域的角度看这题:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

上面说过, javascript采用的是静态作用域, 所以输出的是1

执行上下文

javascript引擎在执行代码时并不是一行一行去执行的,而是一块一块去执行的,引擎在一块一块去执行之前, 会先进行一个准备工作,就是词法分析,比如变量提升,函数提升,重复提升过滤等。《你不知道的jacvascript》专门有讲javascript词法分析。

举个例子: 我们可以把一个js文件看作是一块代码, javascript引擎在执行这块代码之前会进行一个词法分析, 然后再去执行

console.log(value) // undefined
var value = 'hello world'
复制代码

这就是为什么上面代码输出是 undefined 而不是 value is not defined

这里有个疑问就是javascript是怎么区分一块一块代码的呢? 在es6之前, 就三种: 全局代码函数eval代码

这里又要引入一个 执行上下文栈(Execution context stack,ECS))的概念, 假设

ECS = []

用一个数组来表示执行上下文栈 也就是说, javascript引擎在执行全局代码的时候首先会向 ECS栈中压入全局上下文(globalContext), 之后遇到函数时,又向ECS中压入函数执行上下文,当函数执行完毕,再从ECS中弹出对应执行上下文

javascript引擎就是通过执行上下文栈来管理各种执行上下文的

执行上下文中有三个重要的属性:

  • 变量对象(Variable object,VO)

  • this

  • 作用域链(作用域链)

variable object + all parents scopes, 即包含变量对象和所有父级维护的私有作用域([[scope]])

每个执行上下文都会分配一个变量对象,变量对象的属性由变量声明和函数声明构成, 在函数执行上下文中, 变量对象还会包括形参(argument)

在函数执行上下文中,还会多一个活动对象(activation object,ao)的概念。

当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化

即: 在函数上下文中:

variableObject = activationObject + variableObject

举个例子:

function test(num){
    var a = "2";
    return a+num;
}
复制代码

在上面test函数上下文中,变量对象不仅包含了变量 a, 还包括了形参 num, 简单来说, 可以将变量对象和活动对象看作是一个东西, 只不过函数的变量对象还要加上形参。 (这里说的不对, 不过可以这样去简单记忆 - -)

关于作用域链: 每个函数在创建的时候, 函数内部都会维护一个[[scope]]的属性

举个例子:

function foo() {
    function bar() {
        ...
    }
}
复制代码

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];
复制代码

这个可能不是很直观: 再看一个例子:

作用域链.png

最后看一个经典题(简化了一下):

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope()
复制代码

上面输出是什么? 具体执行过程是什么?

  • 进入全局环境上下文,全局环境被压入环境栈,contextStack = [globalContext]

  • 全局上下文环境初始化,

globalContext={
    variable object:[scope, checkscope],
    scope chain: variable object // 全局作用域链
}
复制代码

同时checkscope函数被创建,此时 checkscope.[[Scope]] = globalContext.scopeChain

  • 执行checkscope函数,进入checkscope函数上下文,checkscope被压入环境栈,contextStack=[checkscopeContext, globalContext]。随后checkscope上下文被初始化,它会复制checkscope函数的[[Scope]]变量构建作用域,即 checkscopeContext={ scopeChain : [checkscope.[[Scope]]] }

  • checkscope的活动对象被创建 此时 checkscope.activationObject = [arguments], 随后活动对象被当做变量对象用于初始化,checkscope.variableObject = checkscope.activationObject = [arguments, scope, f],随后变量对象被压入checkscope作用域链前端,(checckscope.scopeChain = [checkscope.variableObject, checkscope.[[Scope]] ]) == [[arguments, scope, f], globalContext.scopeChain]

  • 函数f被初始化,f.[[Scope]] = checkscope.scopeChain。

  • checkscope执行流继续往下走到 return f(),进入函数f执行上下文。函数f执行上下文被压入环境栈,contextStack = [fContext, checkscopeContext, globalContext]。函数f重复 第4步 动作。最后 f.scopeChain = [f.variableObject,checkscope.scopeChain]

  • 函数f执行完毕,f的上下文从环境栈中弹出,此时 contextStack = [checkscopeContext, globalContext]。同时返回 scope, 解释器根据f.scopeChain查找变量scope,在checkscope.scopeChain中找到scope(local scope)。

  • checkscope函数执行完毕,其上下文从环境栈中弹出,contextStack = [globalContext]





以上就是我对javascript 执行上下文, 作用域的一些理解, 又很多地方讲的不严谨, 如果需要深挖一下, 建议阅读以下参考文章





参考文章:

一道js面试题引发的思考

JavaScript深入之作用域链

JavaScript深入之执行上下文栈

JavaScript深入之变量对象

JavaScript深入之词法作用域和动态作用域

文章分类
前端
文章标签