JS(4)—— 执行上下文and作用域(变量提升的原理)

78 阅读5分钟

JS引擎解析代码,不是一行一行去解析的,而是一段一段地分析执行。且当执行一段代码时,会进行一些准备工作:创建执行上下文、变量提升、函数提升、创建作用域链。【这些准备工作,用专业点的说法,就是执行上下文】。

先来解释一下作用域是什么?

作用域是指程序源代码中定义变量的区域。规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。作用域有静态(词法作用域)动态作用域之分。

  • 静态(词法)作用域,函数的作用域在函数定义的时候就决定了。
  • 动态作用域,函数的作用域是在函数调用的时候才决定的。
  • 静态作用域下的,函数作用域是定义该函数所在的作用域;而动态作用域下的,函数作用域是调用该函数时所在的作用域!!!!

JS则是静态作用域

var value = 1; 
function foo() { 
console.log(value); 
} 

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

bar(); // 结果是 1

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

作用域链

作用域链 由多个上下文的变量对象组成的链表。  当查找变量时,会先从当前执行上下文的变量对象中查找;如果没有找到,就去父级的执行上下文的变量对象中找,一直找到全局上下文的变量对象,即 window 身上,如果都没有找到,就会报错!!!!

执行上下文(栈)

执行上下文 包括全局上下文函数上下文 ,每个上下文都有一个关联的变量对象,该上下文中定义的所有函数和变量都会储存在该变量对象中。

每个函数都有自己的函数上下文,那那么多函数就对应有很多的函数上下文,那我们是如何来管理这些函数上下文的呢?---- 执行上下文栈 !!!

  • 执行上下文栈的栈底永远是全局上下文【globalContext】

     - 最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext
    
  • 后面,每执行一个函数,就会为该函数创建函数执行上下文,并把该函数执行上下文压入执行上下文栈中;并且,当函数执行完毕后,又会把该函数的执行上下文从执行上下文栈中弹出!!

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

 // 对应的执行上下文栈的变化如下:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

区别:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

 // 对应的执行上下文栈的变化如下:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

每个执行上下文的重要属性

对于每个执行上下文,都有三个重要属性:

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

变量对象

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

  • 全局上下文的变量对象就是全局对象 window,用VO表示!!!
  • 函数上下文的变量对象用活动对象 AO 表示!!!

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

注意: 只有 var 声明的变量才会被加入到 活动对象AO中!!!!

function foo() { 
console.log(a); 
a = 1; 
}

foo(); // 报错,"a" 并没有通过 var 关键字声明,所有不会被存放在自己的 AO 中。 
// 而且打印的时候 a = 1 声明的全局变量还没有加入到全局对象 windows里!!!!

【区】

function bar() {
a = 1;
console.log(a);
}

bar(); // 打印 1 
// 因为 a = 1 声明的全局变量先加入到全局对象 windows里!!!!后面才打印的,打印的时候在自己AO中没找到,会到父级作用域的对象 windows 中找!!!

All in All 总的流程如下:

【函数被定义时】

  • 作用域就已经被确定了,会将自己所在的父级作用域链存在 [[scope]] 属性中

【函数被执行前的准备工作(解析阶段)】

  • 创建函数执行上下文
  • 在该函数上下文中加入作用域链属性(直接复制自己的[[scope]]属性
  • 在该函数上下文中加入变量对象AO属性(除了函数参数,其余值都赋值为 undefined)

【函数执行阶段】

  • 将该函数上下文压入到上下文栈
  • 并在执行过程中,不断改变 活动对象AO里的值

【函数执行完毕】

  • 函数上下文上下文栈中弹出!!!

详见