若是你想成为一个Javascript开发者,那么你必定要知道Javascript程序的内部运行原理。理解执行环境和执行栈是很是重要的,其有助于理解其余Javascript的概念,好比说提高,做用域和闭包等。javascript
固然,理解执行环境和执行栈的概念也将会使你成为一个更好的Javascript开发者。java
闲话少说,立刻开始吧。编程
简单来讲,执行环境就是Javascript代码被计算和执行的环境的一个抽象概念。不管Javascript代码在何时运行,它都会运行在 执行环境中。windows
在Javascript中有三种执行环境的类型。数组
全局执行环境 - 这是一种默认和基础的执行环境。若是代码不在任何的函数中,那么它就是在全局执行环境中。他作了两件事情:首先,它建立了一个全局对象 - windows(若是是浏览器的话),而且把this的值设置到全局对象中。在程序中,只会存在一个全局执行环境。浏览器
函数执行环境 - 每次当函数被调用的时候,就会为该函数建立一个全新的执行环境。每一个函数都有他们本身的执行环境,可是他们仅仅是在函数被调用的时候才会被建立。其能够有任意多个函数执行环境。不管新的执行环境在何时被建立,它都会按照定义的顺序依次执行一系列的步骤,不过这些咱们稍后会讲。闭包
eval函数执行环境 - 在eval函数中执行代码也会得到它本身的执行环境,可是eval并不常常被Javascript开发者所使用,因此这里咱们目前并不打算讨论它。编程语言
执行栈,在其余编程语言中也被称为调用栈,它是一种LIFO(后进先出)的结构,被用于在代码执行阶段存储全部建立过的执行环境。ide
当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 = <ref. to LexicalEnvironment in memory>, VariableEnvironment = <ref. to VariableEnvironment in memory>, }
官方ES6文档定义的词法环境以下:
词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和一个对外部词汇环境的可能的空引用组成。
简单来讲,词法环境是一个保存“变量-标识符”映射的结构。(标识符指向变量/函数的名称,变量是实际对象【包括函数对象和数组对象】的引用,或者是原始值)
例如,思考下面的代码片断:
var a = 20; var b = 40; function foo() { console.log('bar'); }
上面的代码片断的词法环境以下:
lexicalEnvironment = { a: 20, b: 40, foo: <ref. to foo function> }
每个词法环境都有三组件:
环境记录
对外层环境的引用
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指向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: <null>, this: <global object> } } FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here } outer: <Global or outer function environment reference>, this: <depends on how function is called> } }
它也是一个词法环境,其环境记录中环境记录保存着在运行环境中的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: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
在运行阶段,变量赋值已经完成。所以全局执行环境在执行阶段看起来就像是这样的:
GlobalExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here a: 20, b: 30, multiply: < func > } outer: <null>, ThisBinding: <Global Object> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // Identifier bindings go here c: undefined, } outer: <null>, ThisBinding: <Global Object> } }
当遇到函数multiply(20,30)的调用时,一个新的函数执行环境被建立并执行函数中的代码。所以函数执行环境在建立阶段看起来像是这样子的:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here g: undefined }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
在这之后,执行环境会经历执行阶段,这意味着在函数内部赋值给变量的过程已经完成。所以此函数执行环境在执行阶段看起来就像这样的:
FunctionExectionContext = { LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined>, }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // Identifier bindings go here g: 20 }, outer: <GlobalLexicalEnvironment>, ThisBinding: <Global Object or undefined> } }
在函数执行完成之后,返回值会被存储在c里。所以全局词法环境被更新。在这以后,全局代码执行完成,程序运行终止。
注意:正如你所注意到的,let和const在建立阶段定义的变量没有值与他们相关联,可是var定义变量会设置为false。
这是由于,在建立阶段,扫描代码以查找变量和函数声明,当函数定义被所有存储到环境中时,变量首先会被初始化为undefined(在var的状况中),或者保持未初始化状态(在let和const的状况中)。
这就是你在他们定义以前(虽然是undefined)访问var定义的变量,可是当你在定义以前访问let和const定义的变量时,会获得一个引用错误。
这就是咱们所谓的提高。
注意 - 在执行阶段,若是javascript引擎在源代码中声明的实际位置找不到let变量的值,那么它将为其分配未定义的值。
因此咱们已经讨论了如何在内部执行JavaScript程序。 虽然您没有必要将全部这些概念都学习成为一名出色的JavaScript开发人员,但对上述概念有一个正确的理解将有助于您更轻松,更深刻地理解其余概念,如提高,做用域和闭包。
翻译自: