带你完全搞懂执行上下文

执行上下文

执行上下文 能够理解为当前代码的执行环境,同一个函数在不一样的环境中执行,会由于访问数据的不一样产生不同的结果。

执行上下文分为三种:前端

  • 全局执行上下文:只有一个,程序首次运行时建立,它会在浏览器中建立一个全局对象(window对象),使this指向这个全局对象
  • 函数执行上下文:函数被调用时建立,每次调用都会为该函数建立一个新的执行上下文
  • Eval 函数执行上下文:运行eval函数中的代码时建立的执行上下文,少用且不建议使用

执行上下文栈

执行上下文栈(Execution context stack,ECS),也叫函数调用栈(call stack),是一种拥有 LIFO(后进先出)数据结构的栈,用于存储代码执行时建立的执行上下文数组

因为JS是单线程的,每次只能作一件事情,经过这种机制,咱们可以追踪到哪一个函数正在执行,其余函数在调用栈中排队等待执行。浏览器

JS引擎第一次执行脚本时,会建立一个全局执行上下文压到栈顶,而后随着每次函数的调用都会建立一个新的执行上下文放入到栈顶中,随着函数执行完毕后被执行上下文栈顶弹出,直到回到全局的执行上下文中。数据结构

代码实例闭包

var color = 'blue';

function changeColor() {
  var anotherColor = 'red';

  function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
  }

  swapColors();
}

changeColor();

console.log(color); // red

执行过程能够在 devToolcall stack 中看到,其中 anonyomus 为全局上下文栈;其他为函数上下文栈app

图解: 函数

执行过程:this

  1. 首先建立了全局执行上下文,压入执行栈,其中的可执行代码开始执行。
  2. 而后调用 changeColor 函数,JS引擎中止执行全局执行上下文,激活函数 changeColor 建立它本身的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。
  3. changeColor 调用了 swapColors 函数,此时暂停了 changeColor 的执行上下文,建立了 swapColors 函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。
  4. swapColors 函数执行完后,其执行上下文从栈顶出栈,回到了 changeColor 执行上下文中继续执行。
  5. changeColor 没有可执行代码,也没有再遇到其余执行上下文了,将其执行上下文从栈顶出栈,回到了 全局执行上下文 中继续执行。
  6. 一旦全部代码执行完毕,JS引擎将从当前栈中移除 全局执行上下文
注意:函数中,遇到return能直接终止可执行代码的执行,所以会直接将当前上下文弹出栈。

使用 ECStack 来模拟调用栈:es5

ECStack=[]

JS第一次执行代码时就会遇到全局代码,执行上下文栈会压入一个全局上下文,咱们用 globalContext 表示它,只有当整个应用程序结束的时候,ECStack 才会被清空,因此 ECStack 最底部永远有个 globalContextspa

ECStack.push(globalContext)

使用伪代码模拟上述代码行为:

ECStack.push(<changeColor> functionContext);
ECStack.push(<swapColors> functionContext);

// swapColors出栈
ECStack.pop();
// changeColor出栈
ECStack.pop();

为了巩固一下执行上下文的理解,咱们再来绘制一个例子的演变过程,这是一个简单的闭包例子。

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
f1()() // 999

使用伪代码模拟上述代码行为:

ECStack.push(<f1> functionContext);
// f1出栈
ECStack.pop();

ECStack.push(<f2> functionContext);
// f2出栈
ECStack.pop();

由于f1中的函数f2在f1的可执行代码中,并无被调用执行,所以执行f1时,f2不会建立新的上下文,而直到f2执行时,才建立了一个新的。具体演变过程以下。

es3版本

es3版本执行上下文内有三个重要属性:

  • 变量对象 VO(variable object)
  • 做用域链(scope chain)
  • this

能够将每一个执行上下文抽象为一个对象。

执行上下文的组成代码示例:

executionContextObj = {
  scopeChain: { /* 变量对象(variableObject)+ 全部父执行上下文的变量对象*/ },
  [variableObject | activationObject]: {
    /*函数 arguments/参数,内部变量和函数声明 */
    arguments,
    ...
  },
  this: {}
}

变量对象

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

不一样执行上下文中的变量对象也不同:

  • 全局上下文 中的变量对象就是全局对象,在浏览器中就是 window 对象。在顶层 JavaScript 代码中,能够用关键字 this 引用全局对象。全部的全局变量和函数都是做为 window 的属性和方法存在。
console.log(this) //window
    var a=1 //挂到window上的属性
    console.log(window.a) //1
    console.log(this.a) //1
  • 函数执行上下文 中咱们用活动对象 AO (activation object) 来表示变量对象,由于变量对象是规范上的或者说是引擎实现上的,在 JavaScript 环境中是不能被直接访问的,只有当函数被调用时,变量对象被激活为活动对象时,咱们才能访问到其中的属性和方法。
活动对象就是变量对象,只不过处于不一样的状态和阶段而已。

做用域链

对于 JavaScript 来讲做用域及做用域链的变量查询是经过存储在浏览器内存中的执行上下文实现的。当查找变量时,首先从当前上下文中的变量对象查找,若是没有就会往上查找父级做用域中的变量对象,最终找到全局上下文的变量对象,若是没有就报错。这样由多个执行上下文的变量对象构成的链表就叫作做用域链

那么有同窗就有疑问了,做用域和执行上下文有什么 区别 呢 :

函数执行上下文是在调用函数时, 函数体代码执行以前建立,函数调用结束时就会自动释放。由于不一样的调用可能有不一样的参数:

var a = 10;
function fn(x) {
  var a = 20;
  console.log(arguments)
  console.log(x)
}
fn(20)
fn(10) // 不一样的调用可能有不一样的参数

而JavaScript采用的是词法做用域,fn 函数建立的做用域在函数定义时就已经肯定了;

关联

做用域只是一个“地盘”,其中没有变量,要经过做用域对应的执行上下文环境来获取变量的值,因此做用域是静态观念的,而执行上下文环境是动态的。也就是说,做用域只是用于划分你在这个做用域里面定义的变量的有效范围,出了这个做用域就无效。

同一个做用域下,对同一个函数的不一样的调用会产生不一样的执行上下文环境,继而产生不一样的变量的值,因此,做用域中变量的值是在执行过程当中肯定的,而做用域是在函数建立时就肯定的。

生命周期

执行上下文的生命周期有三个阶段,分别是:

  • 建立阶段

    • 生成变量对象

      • 建立arguments
      • 扫描函数声明
      • 扫描变量声明
    • 创建做用域链
    • 肯定this的指向
  • 执行阶段

    • 变量赋值
    • 函数的引用
    • 执行其余代码
  • 销毁阶段

建立阶段

生成变量对象

  1. 建立arguments:若是是函数上下文,首先会建立 arguments 对象,给变量对象添加形参名称和值。
  2. 扫描函数声明:对于找到的函数声明,将函数名和函数引用(指针)存入 VO 中,若是 VO 中已经有同名函数,那么就进行覆盖(重写引用指针)。
  3. 扫描变量声明:对于找到的每一个变量声明,将变量名存入 VO 中,而且将变量的值初始化为undefined 。若是变量的名字已经在变量对象里存在,不会进行任何操做并继续扫描。

让咱们举一个栗子来讲明 :

function person(age) {
  console.log(typeof name); // function
  console.log(typeof getName); // undefined
  var name = 'abby';
  var hobby = 'game';
  var getName = function getName() {
    return 'Lucky';
  };
  function name() {
    return 'Abby';
  }
  function getAge() {
    return age;
  }
  console.log(typeof name); // string
  console.log(typeof getName); // function
  name = function () {};
  console.log(typeof name); // function
}
person(20);

在调用person(20)的时候,可是代码还没执行的时候,建立的状态是这样:

personContext = {
    scopeChain: { ... },
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: undefined,
        getName : undefined,
    },
    this: { ... }
}

函数在执行以前,会先建立一个函数执行上下文,首先是指出函数的引用,而后按顺序对变量进行定义,初始化为 undefined存入到 VO 之中,在扫描到变量 name 时发如今 VO 之中存在同名的属性(函数声明变量),所以忽略。

全局执行上下文的建立没有建立 arguments 这一步

创建做用域链

在执行期上下文的建立阶段,做用域链是在变量对象以后建立的。做用域链自己包含变量对象。

  1. 当书写一段函数代码时,就会建立一个词法做用域,这个做用域是函数内部的属性,咱们用[[scope]]表示,它里面保存父变量对象,因此[[scope]]就是一条层级链。
person.[[scope]] = [
     globalContext.variableObject
]
  1. 当函数调用,就意味着函数被激活了,此时建立函数上下文并压入执行栈,而后复制函数 [[scope]] 属性建立做用域链:
personContext = {
     scopeChain:person.[[scope]]
}
  1. 建立活动对象(前面的生成变量对象步骤),而后将活动对象(AO)推到做用域链的前端。
personContext = {
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: undefined,
        getName : undefined,
    },
    scopeChain:[activationObject,[[scope]]]
}

肯定this的指向

若是当前函数被做为对象方法调用或使用 bindcallapplyAPI 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,不然默认为全局对象调用。

执行阶段

执行阶段 中,执行流进入函数而且在上下文中运行/解释代码,JS 引擎开始对定义的变量赋值、开始顺着做用域链访问变量、若是内部有函数调用就建立一个新的执行上下文压入执行栈并把控制权交出

此时代码从上到下执行的时候激活阶段的过程是:

  1. 第一次执行 console.log; 此时 nameVO 中是函数。getName 未指定值在 VO 中的值是 undefined
  2. 执行到赋值代码,getName 被赋值成函数表达式,name 被赋值为 abby
  3. 第二次执行 console.log; 此时的 name 因为函数被字符串赋值覆盖所以是 string 类型getNamefunction 类型。
  4. 第三次执行 console.log; 此时的 name 因为又被覆盖所以是 function 类型

所以理解执行上下文以后很好解释了变量提高(Hoisting):实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被JavaScript引擎放入内存中

这就解释了为何咱们能在 name 声明以前访问它,为何以后的 name 的类型值发生了变化,为何 getName 第一次打印的时候是 undefined 等等问题了。

ES6 引入了 letconst 关键字,从而使 JavaScript 也能像其余语言同样拥有了块级做用域,很好解决了变量提高带来的一系列问题。

最后执行 console 时候的函数执行上下文:

personContext = {
    scopeChain: { ... },
    activationObject: {
        arguments: {
            0: 20,
            length: 1
        },
        age: 20,
        name: pointer, // reference to function name(),
        getAge: pointer, // reference to function getAge(),
        hobby: 'game',
        getName:pointer, pointer to function getName(),
    },
    this: { ... }
}

销毁阶段

通常来说当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈而且等待虚拟机回收,控制权被从新交给执行栈上一层的执行上下文。

完整示例

示例一

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

一、执行全局代码,生成全局上下文,而且压入执行栈

ECStack=[
     globalContext
]
复制代码

二、全局上下文初始化

globalContext={
     variableObject:[global,scope,checkscope],
     this:globalContext.variableObject,
     scopeChain:[globalContext.variableObject]
}

三、建立 checkscope 函数时生成内部属性 [[scope]],并将全局上下文做用域链存入其中

checkscope.[[scope]] = [
     globalContext.variableObject
]

四、调用 checkscope 函数,建立函数上下文,压栈

ECStack=[
     globalContext,
     checkscopeContext
]

五、此时 checkscope 函数还未执行,进入执行上下文

  • 复制函数 [[scope]] 属性建立做用域链
  • 用 arguments 属性建立活动对象
  • 初始化变量对象,加入变量声明、函数声明、形参
  • 活动对象压入做用域链顶端
checkscopeContext = {
        activationObject: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: pointer, // reference to function f(),
        },
        scopeChain: [activationObject, globalContext.variableObject],
        this: undefined
    }

六、checkscope 函数执行,对变量 scope 设值

checkscopeContext = {
        activationObject: {
            arguments: {
                length: 0
            },
            scope: 'local scope',
            f: pointer, // reference to function f(),
        },
        scopeChain: [activationObject, globalContext.variableObject],
        this: undefined
    }

f 函数被建立生成 [[scope]] 属性,并保存父做用域的做用域链

f.[[scope]]=[
     checkscopeContext.activationObject,
     globalContext.variableObject
]

七、f 函数调用,生成 f 函数上下文,压栈

ECStack=[
     globalContext,
     checkscopeContext,
     fContext
]

八、此时 f 函数还未执行,初始化执行上下文

  • 复制函数 [[scope]] 属性建立做用域链
  • 用 arguments 属性建立活动对象
  • 初始化变量对象,加入变量声明、函数声明、形参
  • 活动对象压入做用域链顶端
fContext = {
     activationObject: {
            arguments: {
                length: 0
            },
        },
        scopeChain: [fContext.activationObject, checkscopeContext.activationObject, globalContext.variableObject],
        this: undefined
    }

九、f 函数执行,沿着做用域链查找 scope 值,返回 scope 值

十、f 函数执行完毕,f函数上下文从执行上下文栈中弹出

ECStack=[
     globalContext,
     checkscopeContext
]

十一、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack=[
     globalContext
]

示例二

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
  1. 执行全局代码,生成全局上下文,而且压入执行栈
  2. 全局上下文初始化
  3. 建立 checkscope 函数时生成内部属性 [[scope]],并将全局上下文做用域链存入其中
  4. 调用 checkscope 函数,建立函数上下文,压栈
  5. 此时 checkscope 函数还未执行,进入执行上下文

    • 复制函数 [[scope]] 属性建立做用域链
    • arguments 属性建立活动对象
    • 初始化变量对象,加入变量声明、函数声明、形参
    • 活动对象压入做用域链顶端
  6. checkscope 函数执行,对变量 scope 设值,f 函数被建立生成 [[scope]] 属性,并保存父做用域的做用域链
  7. 返回函数f,此时 checkscope 函数执行完成,弹栈
  8. f 函数调用,生成 f 函数上下文,压栈
  9. 此时 f 函数还未执行,初始化执行上下文

    • 复制函数 [[scope]] 属性建立做用域链
    • arguments 属性建立活动对象
    • 初始化变量对象,加入变量声明、函数声明、形参
    • 活动对象压入做用域链顶端
  10. f 函数执行,沿着做用域链查找 scope 值,返回 scope
  11. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

    能够看到和前面惟一的区别就是 checkScope 函数执行完先出栈了,以后再执行 f 函数,步骤与示例一一致

fContext = {
    scopeChain: [activationObject, checkscopeContext.activationObject, globalContext.variableObject],
}

这里在 checkscopeContext 函数执行完销毁后,f 函数依然能够读取到 checkscopeContext.AO 的值,也就是说 checkscopeContext.AO 依然活在内存中,f 函数依然能够经过 f 函数的做用域链找到它。而为何 checkscopeContext.AO 没有被销毁,正是由于 f 函数引用了 checkscopeContext.AO 中的值,又正是由于JS实现了在子上下文引用父上下文的变量的时候,不会销毁这些变量的效果实现了闭包 这个概念!

es5版本

ES5 规范去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。

生命周期

es5 执行上下文的生命周期也包括三个阶段:建立阶段 → 执行阶段 → 回收阶段

建立阶段

建立阶段作了三件事:

  1. 肯定 this 的值,也被称为 This Binding
  2. LexicalEnvironment(词法环境) 组件被建立
  3. VariableEnvironment(变量环境) 组件被建立

伪代码大概以下:

ExecutionContext = {  
  ThisBinding = <this value>,     // 肯定this 
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}
This Binding

ThisBinding 是和执行上下文绑定的,也就是说每一个执行上下文中都有一个 this,与 es3this 并无什么区别,this 的值是在执行的时候才能确认,定义的时候不能确认

建立词法环境

词法环境的结构以下:

GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {     // 环境记录
      Type: "Object",           // 全局环境
      // 标识符绑定在这里 
      outer: <null>           // 对外部环境的引用
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {     // 词法环境
    EnvironmentRecord: {    // 环境记录
      Type: "Declarative",      // 函数环境
      // 标识符绑定在这里      // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

能够看到词法环境有两种类型 :

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用能够是全局环境,也能够是包含内部函数的外部函数环境。

词法环境有两个组件 :

  • 环境记录器 :存储变量和函数声明的实际位置。
  • 外部环境的引用 :它指向做用域链的下一个对象,能够访问其父级词法环境(做用域),做用与 es3 的做用域链类似

环境记录器也有两种类型 :

  • 在函数环境中使用 声明式环境记录器,用来存储变量、函数和参数。
  • 在全局环境中使用 对象环境记录器,用来定义出如今全局上下文中的变量和函数的关系。

    所以:

  • 建立全局上下文的词法环境使用 对象环境记录器 ,outer 值为 null;
  • 建立函数上下文的词法环境时使用 声明式环境记录器 ,outer 值为全局对象,或者为父级词法环境(做用域)
建立变量环境

变量环境也是一个词法环境,所以它具备上面定义的词法环境的全部属性。

在 ES6 中,词法环境和 变量环境的区别在于前者用于存储函数声明和变量( letconst关键字)绑定,然后者仅用于存储变量( var )绑定,所以变量环境实现函数级做用域,经过词法环境在函数做用域的基础上实现块级做用域。

🚨 使用 let / const 声明的全局变量,会被绑定到 Script 对象而不是 Window 对象,不能以Window.xx 的形式使用;使用 var 声明的全局变量会被绑定到 Window 对象;使用 var / let / const 声明的局部变量都会被绑定到 Local 对象。注:Script 对象、Window 对象、Local 对象三者是平行并列关系。

箭头函数没有本身的上下文,没有arguments,也不存在变量提高

使用例子进行介绍

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

遇到调用函数 multiply 时,函数执行上下文开始被建立:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

变量提高的缘由:在建立阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的状况下)或保持未初始化 uninitialized(在 let 和 const 的状况下)。因此这就是为何能够在声明以前访问 var 定义的变量(尽管是 undefined ),但若是在声明以前访问 let 和 const 定义的变量就会提示引用错误的缘由。这就是所谓的变量提高。

图解变量提高:

var myname = "极客时间"
function showName(){
  console.log(myname);
  if(0){
   var myname = "极客邦"
  }
  console.log(myname);
}
showName()

在 showName 内部查找 myname 时会先使用当前函数执行上下文里面的变量 myname ,因为变量提高,当前的执行上下文中就包含了变量 myname,而值是 undefined,因此获取到的 myname 的值就是 undefined。

执行阶段

在此阶段,完成对全部这些变量的分配,最后执行代码,若是 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

回收阶段

执行上下文出栈等待虚拟机回收执行上下文

过程总结

  1. 建立阶段 首先建立全局上下文的词法环境:首先建立 对象环境记录器,接着建立他的外部环境引用 outer,值为 null
  2. 建立全局上下文的语法环境:过程同上
  3. 肯定 this 值为全局对象(以浏览器为例,就是 window )
  4. 函数被调用,建立函数上下文的词法环境:首先建立 声明式环境记录器,接着建立他的外部环境引用 outer,值为 null,值为全局对象,或者为父级词法环境
  5. 建立函数上下文的变量环境:过程同上
  6. 肯定 this 值
  7. 进入函数执行上下文的 执行阶段
  8. 执行完成后进入 回收阶段

实例讲解

将词法环境中 outer 抽离出来,执行上下文结构以下:

下面咱们以以下示例来分析执行上下文的建立及执行过程:

function foo(){
  var a = 1
  let b = 2
  {
    let b = 3
    var c = 4
    let d = 5
    console.log(a)
    console.log(b)
  }
  console.log(b) 
  console.log(c)
  console.log(d)
}   
foo()

第一步: 调用 foo 函数前先编译并建立执行上下文,在编译阶段将 var 声明的变量存放到变量环境中,let 声明的变量存放到词法环境中,须要注意的是此时在函数体内部块做用域中 let 声明的变量不会被存放到词法环境中,以下图所示 :

第二步: 继续执行代码,当执行到代码块里面时,变量环境中的 a 的值已经被设置为1,词法环境中 b 的值已经被设置成了2,此时函数的执行上下文如图所示:

从图中就能够看出,当进入函数的做用域块时,做用域块中经过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响做用域块外面的变量,所以示例中在函数体内块做用域中声明的变量的 b 与函数做用域中声明的变量 b 都是独立的存在。

在词法环境内部,实际上维护了一个小型栈结构,栈底是函数最外层的变量,进入一个做用域块后,就会把该做用域内部的变量压到栈顶;当该块级做用域执行完成以后,该做用域的信息就会从栈顶弹出,这就是词法环境的结构。

第三步: 当代码执行到做用域块中的 console.log(a) 时,就须要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,若是在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,若是没有查找到,那么继续在变量环境中查找。

这样一个变量查找过程就完成了,你能够参考下图:

第四步: 当函数体内块做用域执行结束以后,其内部变量就会从词法环境的栈顶弹出,此时执行上下文以下图所示:

第五步: 当foo函数执行完毕后执行栈将foo函数的执行上下文弹出。

因此,块级做用域就是经过词法环境的栈结构来实现的,而变量提高是经过变量环境来实现,经过这二者的结合,JavaScript 引擎也就同时支持了变量提高和块级做用域了。

outer引用

outer 是一个外部引用,用来指向外部的执行上下文,其是由词法做用域指定的
function bar() {
  console.log(myName)
}
function foo() {
  var myName = " 极客邦 "
  bar()
}
var myName = " 极客时间 "
foo()

当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量, 好比上面那段代码在查找 myName 变量时,若是在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。为了直观理解,你能够看下面这张图:

从图中能够看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着若是在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。咱们把这个查找的链条就称为做用域链。 如今你知道变量是经过做用域链来查找的了,不过还有一个疑问没有解开,foo 函数调用的 bar 函数,那为何 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

这是由于在 JavaScript 执行过程当中,其做用域链是由词法做用域决定的。词法做用域指做用域是由代码中函数声明的位置来决定的,所以是静态的做用域

结合变量环境、词法环境以及做用域链,咱们看下下面的代码:

function bar() {
  var myName = " 极客世界 "
  let test1 = 100
  if (1) {
    let myName = "Chrome 浏览器 "
    console.log(test)
  }
}
function foo() {
  var myName = " 极客邦 "
  let test = 2
  {
    let test = 3
    bar()
  }
}
var myName = " 极客时间 "
let myAge = 10
let test = 1
foo()

对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的状况以下图所示:

解释下这个过程。首先是在 bar 函数的执行上下文中查找,但由于 bar 函数的执行上下文中没有定义 test 变量,因此根据词法做用域的规则,下一步就在 bar 函数的外部做用域中查找,也就是全局做用域。

相关文章
相关标签/搜索