资讯专栏INFORMATION COLUMN

深入理解Javascript中的执行环境(Execution Context)和执行栈(Execut

whidy / 1666人阅读

摘要:引擎会执行其执行环境位于堆栈顶部的函数。当函数执行完毕时,当前执行栈会从堆栈中弹出去,并且控件将会到达其在当前堆栈下面的那个执行环境中。当完成以后,它的执行环境会会从堆栈中移出,并且控件会到达全局执行环境。

如果你想成为一个Javascript开发者,那么你一定要知道Javascript程序的内部运行原理。理解执行环境和执行栈是非常重要的,其有助于理解其他Javascript的概念,比如说提升,作用域和闭包等。

当然,理解执行环境和执行栈的概念也将会使你成为一个更好的Javascript开发者。

闲话少说,马上开始吧。

执行环境是什么

简单来说,执行环境就是Javascript代码被计算和执行的环境的一个抽象概念。无论Javascript代码在什么时候运行,它都会运行在 执行环境中。

执行环境的类型

在Javascript中有三种执行环境的类型。

全局执行环境 - 这是一种默认和基础的执行环境。如果代码不在任何的函数中,那么它就是在全局执行环境中。他做了两件事情:首先,它创建了一个全局对象 - windows(如果是浏览器的话),并且把this的值设置到全局对象中。在程序中,只会存在一个全局执行环境。

函数执行环境 - 每次当函数被调用的时候,就会为该函数创建一个全新的执行环境。每个函数都有他们自己的执行环境,但是他们仅仅是在函数被调用的时候才会被创建。其可以有任意多个函数执行环境。无论新的执行环境在什么时候被创建,它都会按照定义的顺序依次执行一系列的步骤,不过这些我们稍后会讲。

eval函数执行环境 - 在eval函数中执行代码也会获得它自己的执行环境,但是eval并不经常被Javascript开发者所使用,所以这里我们目前并不打算讨论它。

执行栈

执行栈,在其他编程语言中也被称为调用栈,它是一种LIFO(后进先出)的结构,被用于在代码执行阶段存储所有创建过的执行环境。

当Javascript引擎首次运行到你的脚本时,它会创建一个全局执行环境,并把它推入到当前的执行栈中。每当引擎运行到其函数调用时,就会为这个函数创建一个新的执行环境,并把它推入到堆栈的顶部。

引擎会执行其执行环境位于堆栈顶部的函数。当函数执行完毕时,当前执行栈会从堆栈中弹出去,并且控件将会到达其在当前堆栈下面的那个执行环境中。

我们来通过下面的代码示例来理解:

let a = "Hello World!";
function first() {
  console.log("Inside first function");
  second();
  console.log("Again inside first function");
}
function second() {
  console.log("Inside second function");
}
first();
console.log("Inside Global Execution Context");

当上面的代码加载到浏览器中时,Javascript引擎会创建一个全局执行环境,并把它推到当前的执行栈中。当遇到对first()的调用时,Javascript引擎会为这个函数创建一个新的执行环境,并且把它推到当前执行栈的顶部。

当second()函数在first()函数内被调用时,Javascript引擎会为这个函数创建一个新的执行环境,并把它推送到当前执行栈的顶部。当second()函数完成的时候,它的执行环境会从当前的栈中推出去,并且空间会到达当前环境下面的那个执行环境中,也就是first()函数执行环境。

当first()完成以后,它的执行环境会会从堆栈中移出,并且控件会到达全局执行环境。当所有代码执行完以后,Javascript引擎会从当前栈中移出全局执行环境。

那么执行环境是如何被创建出来的呢?

到现在为止,我们已经看到Javascript引擎是如何管理执行环境的。那么现在咱们来理解一下执行环境是如何被Javascript引擎创建出来的吧。

执行环境的创建过程分为两个阶段:1,创建阶段,2,执行阶段。

创建阶段

执行环境是在创建阶段被创建出来的。在创建阶段会发生下面的事情:

词法环境组件被创建出来。

变量环境组件被创建出来。

因此执行环境从概念上可以被表示为:

ExecutionContext = {
  LexicalEnvironment = ,
  VariableEnvironment = ,
}


词法环境

官方ES6文档定义的词法环境如下:

词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和一个对外部词汇环境的可能的空引用组成。

简单来说,词法环境是一个保存“变量-标识符”映射的结构。(标识符指向变量/函数的名称,变量是实际对象【包括函数对象和数组对象】的引用,或者是原始值)

例如,思考下面的代码片段:

var a = 20;
var b = 40;
function foo() {
  console.log("bar");
}

上面的代码片段的词法环境如下:

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: 
}

每一个词法环境都有三组件:

环境记录

对外层环境的引用

this绑定

环境记录

环境记录是变量和函数声明的地方,其被存储在词法环境内部。

有两种词法环境的类型:

声明环境记录 - 顾名思义,它存储变量和函数的声明。函数代码的词法环境包含一个声明环境记录。

对象环境记录 - 全局代码的词法环境包含一个对象环境记录。除了变量和函数声明之外,对象环境记录也会存储全局绑定对象(浏览器中的window对象)。因此对于每个绑定对象的属性(对于浏览器,它包含所有由浏览器给window对象的属性和方法),在记录中创建一个新的条目。

注意 - 对于函数代码,环境记录也会包含参数对象,参数对象包含传递给函数的参数以及索引,和传递给函数的参数的长度(个数)。例如,下面函数的参数对象看起来像这样子的:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},


对外部环境的引用

对外部环境的引用意味着它可以访问外面的词法环境。这意味着如果他们在当前的词法环境中没有找到的话,Javascript引擎会在外面的环境里去寻找变量。

this绑定

在这个组件中,this的值是确定的或者是已经设置的。

在全局执行环境中,this的值指向全局对象。(在浏览器中,this指向window对象)

在函数执行环境中,this的值依赖于函数的调用方式。如果它是在对象引用中被调用,this的值就被设置为那个对象,否则,this的值会被设置为全局对象或者是undefined(在严格模式中)。例如:

const person = {
  name: "peter",
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge();
// "this" refers to "person", because "calcAge" was called with //"person" object reference
const calculateAge = person.calcAge;
calculateAge();
// "this" refers to the global window object, because no object reference was given

抽象的说,在伪代码中,词法环境看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: ,
    this: 
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: ,
    this: 
  }
}


变量环境:

它也是一个词法环境,其环境记录中环境记录保存着在运行环境中的VariableStatements创建的绑定。

正如上面所写的,变量环境也是一个词法环境,因此他有如上定义的词法环境的所有的属性和组件。

在ES6中,词法环境组件和变量环境组件的一个不同点就是前者被用于存储函数声明和变量(let,const)的绑定。而后者只被用于存储变量(var)的绑定。

执行阶段

在这个阶段,所有的变量赋值都会完成,所有的代码最终也都会执行完毕。

例子

我们来看一些例子来理解上面的概念。

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
  var g = 20;
  return e * f * g;
}
c = multiply(20, 30);

当上面的代码被执行的时候,Javascript引擎会创建一个全局的执行环境来执行这些全局代码。因此全局执行环境在创建阶段看起来像这样子的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
    b: < uninitialized >,
    multiply: < func >
  }
  outer: ,
    ThisBinding: 
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: , 
    ThisBinding: 
  }
}

在运行阶段,变量赋值已经完成。因此全局执行环境在执行阶段看起来就像是这样的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: ,
    ThisBinding: 
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: ,
    ThisBinding: 
  }
}

当遇到函数multiply(20,30)的调用时,一个新的函数执行环境被创建并执行函数中的代码。因此函数执行环境在创建阶段看起来像是这样子的:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: ,
    ThisBinding: ,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: ,
    ThisBinding: 
  }
}

在这以后,执行环境会经历执行阶段,这意味着在函数内部赋值给变量的过程已经完成。因此此函数执行环境在执行阶段看起来就像这样的:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: ,
    ThisBinding: ,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: ,
    ThisBinding: 
  }
}

在函数执行完成以后,返回值会被存储在c里。因此全局词法环境被更新。在这之后,全局代码执行完成,程序运行终止。

注意:正如你所注意到的,let和const在创建阶段定义的变量没有值与他们相关联,但是var定义变量会设置为false。

这是因为,在创建阶段,扫描代码以查找变量和函数声明,当函数定义被全部存储到环境中时,变量首先会被初始化为undefined(在var的情况中),或者保持未初始化状态(在let和const的情况中)。

这就是你在他们定义之前(虽然是undefined)访问var定义的变量,但是当你在定义之前访问let和const定义的变量时,会得到一个引用错误。

这就是我们所谓的提升。

注意 - 在执行阶段,如果javascript引擎在源代码中声明的实际位置找不到let变量的值,那么它将为其分配未定义的值。

结论

所以我们已经讨论了如何在内部执行JavaScript程序。 虽然您没有必要将所有这些概念都学习成为一名出色的JavaScript开发人员,但对上述概念有一个正确的理解将有助于您更轻松,更深入地理解其他概念,如提升,作用域和闭包。

翻译自:

https://blog.bitsrc.io/unders...

转载自:http://www.lht.ren/article/18/

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101959.html

相关文章

  • 深入学习js之——执行上下文

    摘要:当遇到函数调用时,引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。参考链接理解中的执行上下文和执行栈深入之执行上下文栈 开篇 作为一个JavaScript的程序开发者,如果被问到JavaScript代码的执行顺序,你脑海中是不是有一个直观的印象 -- JavaScript 是顺序执行的,可事实真的是这样的吗? 让我们首先看两个小例子: var foo = functio...

    Lucky_Boy 评论0 收藏0
  • # JavaScript中的执行上下文队列()的关系?

    摘要:为什么会这样这段代码究竟是如何运行的执行上下文堆栈浏览器中的解释器单线程运行。浏览器始终执行位于堆栈顶部的,并且一旦函数完成执行当前操作,它将从堆栈顶部弹出,将控制权返回到当前堆栈中的下方上下文。确定在上下文中的值。 原文:What is the Execution Context & Stack in JavaScript? git地址:JavaScript中的执行上下文和队列(...

    DangoSky 评论0 收藏0
  • JavaScript基础系列---执行环境与作用域链

    摘要:延长作用域链下面两种语句可以在作用域链的前端临时增加一个变量对象以延长作用域链, 问题 今天看笔记发现自己之前记了一个关于同名标识符优先级的内容,具体是下面这样的: 形参优先级高于当前函数名,低于内部函数名 形参优先级高于arguments 形参优先级高于只声明却未赋值的局部变量,但是低于声明且赋值的局部变量 函数和变量都会声明提升,函数名和变量名同名时,函数名的优先级要高。执行代...

    J4ck_Chan 评论0 收藏0
  • 深入理解JavaScript执行上下文、函数堆、提升的概念

    摘要:原文链接变量对象是说的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被的函数标示符形参变量声明等。对于函数的形参没有什么可说的,主要看一下函数的声明以及变量的声明两个部分。 首先明确几个概念: EC:函数执行环境(或执行上下文),Execution Context ECS:执行环境栈,Execution Context Stack VO:变量对象,Variable Obj...

    hatlonely 评论0 收藏0

发表评论

0条评论

whidy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<