经过javascript 执行环境理解她

从古到今最难的学的武功(javascript)算其一。javascript

欲练此功必先自宫,愿少侠习的此功,笑傲江湖。java

你将了解

  • 执行栈(Execution stack)
  • 执行上下文(Execution Context)
  • 做用域链(scope chains)
  • 变量提高(hoisting)
  • 闭包(closures)
  • this 绑定

执行栈

又叫调用栈,具备 LIFO(last in first out 后进先出)结构,用于存储在代码执行期间建立的全部执行上下文。chrome

当 JavaScript 引擎首次读取你的脚本时,它会建立一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数建立一个新的执行上下文并将其推到当前执行栈的顶端。
引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。数组

咱们经过下面的示例来讲明一下浏览器

function one() {
  console.log('one')
  two()
}
function two() {
  console.log('two')
}
one()

当程序(代码)开始执行时 javscript 引擎建立 GobalExecutionContext (全局执行上下文)推入当前的执行栈,此时 GobalExecutionContext 处于栈顶会马上执行全局执行上下文 而后遇到 one() 引擎都会为该函数建立一个新的执行上下文 oneFunctionExecutionContext 并将其推到当前执行栈的顶端并执行,而后遇到two() twoFunctionExecutionContext 入栈并执行至出栈,回到 oneFunctionExecutionContext 继续执行至出栈 ,最后剩余一个 GobalExecutionContext 它会在程序关闭的时候出栈。闭包

而后调用栈以下图:
imageide

若是是这样的代码函数

function foo() {
  foo()
}
foo()

以下
image
当一个递归没有结束点的时候就会出现栈溢出ui

什么是执行上下文

了解 JavaScript 的执行上下文,有助于你理解更高级的内容好比变量提高、做用域链和闭包。既然如此,那到底什么是“执行上下文”呢?this

执行上下文是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。

Javascript 中代码的执行上下文分为如下三种:

  1. 全局执行上下文(Global Execution Context)- 这个是默认的代码运行环境,一旦代码被载入,引擎最早进入的就是这个环境。
  2. 函数执行上下文(Function Execution Context) - 当执行一个函数时,运行函数体中的代码。
  3. Eval - 在 Eval 函数内运行的代码。

javascript 是一个单线程语言,这意味着在浏览器中同时只能作一件事情。当 javascript 解释器初始执行代码,它首先默认进入全局上下文。每次调用一个函数将会建立一个新的执行上下文。

javascript执行栈中不一样执行上下文之间的词法环境有一种关联关系,从栈顶到栈底(从局部直到全局),这种关系被叫作做用域链

简单的说,每次你试图访问函数执行上下文中的变量时,进程老是从本身上下文环境中开始查找。若是在本身的上下文中没发现要查找的变量,继续搜索下一层上下文。它将检查执行栈中每个执行上下文环境,寻找和变量名称匹配的值,直到找到为止,若是到全局都没有则抛出错误。

执行上下文的建立过程

咱们如今已经知道,每当调用一个函数时,一个新的执行上下文就会被建立出来。然而,在 javascript 引擎内部,这个上下文的建立过程具体分为两个阶段:

建立阶段 > 执行阶段

建立阶段

执行上下文在建立阶段建立。在建立阶段发生如下事情:

  1. LexicalEnvironment 组件已建立。
  2. VariableEnvironment 组件已建立。

所以,执行上下文能够在概念上表示以下:

ExecutionContext = {
  LexicalEnvironment = <词法环境>,
  VariableEnvironment = <变量环境>,
}

词法环境(Lexical Environment)

官方 ES6 文档将词法环境定义为:

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

简而言之,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)

词法环境有两种类型

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

每一个词汇环境都有三个组成部分:

1)环境记录(environment record)

2)对外部环境的引用(outer)

3) 绑定 this

环境记录 一样有两种类型(以下所示):

  • 声明性环境记录 存储变量、函数和参数。一个函数环境包含声明性环境记录。
  • 对象环境记录 用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录

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

词法环境和环境记录值是纯粹的规范机制,ECMAScript 程序不能直接访问或操纵这些值。

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:<取决于函数的调用方式>
  }
}

变量环境:

它也是一个词法环境,其 EnvironmentRecord 包含了由 VariableStatements 在此执行上下文建立的绑定。
如上所述,变量环境也是一个词法环境,所以它具备上面定义的词法环境的全部属性。
在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( 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)

执行上下文以下所示:

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)遇到函数调用时,会建立一个新的函数执行上下文来执行函数代码。所以,在建立阶段,函数执行上下文将以下所示:

// multiply 建立
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

在此以后,执行上下文将执行执行阶段,这意味着完成了对函数内部变量的赋值。所以,在执行阶段,函数执行上下文将以下所示:

// multiply 执行
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函数完成后,返回的值赋值给c。所以,全局词法环境获得了更新。以后,全局代码完成,程序结束。

注: 在执行阶段,若是 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

变量提高

在网上一直看到这样的总结: 在函数中声明的变量以及函数,其做用域提高到函数顶部,换句话说,就是一进入函数体,就能够访问到其中声明的变量以及函数。这是对的,可是知道其中的原因吗?相信你经过上述的解释应该也有所明白了。不过在这边再分析一下。

你可能已经注意到了在建立阶段 letconst 定义的变量没有任何与之关联的值,但 var 定义的变量设置为 undefined
这是由于在建立阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined(在 var 声明变量的状况下)或保持未初始化(在 letconst 声明变量的状况下)。
这就是为何你能够在声明以前访问var 定义的变量(尽管是 undefined ),但若是在声明以前访问letconst 定义的变量就会提示引用错误的缘由。
这就是咱们所谓的变量提高

思考题:

console.log('step1:',a)
var a = 'artiely'
console.log('step2:',a)
function bar (a){
  console.log('step3:',a)
  a = 'TJ'
  console.log('step4:',a)
  function a(){
  }
}
bar(a)
console.log('step5:',a)

对外部环境的引用

上面代码若是咱们改用调用方式以下:

let a = 20
const b = 30
var c

function multiply() {
  var g = 20
  return a * b * g
}

c = multiply()

其实你会发现结果是同样的
可是 multiply 的执行上下文却发生一些变化

// 建立
FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: { length: 0},
    },
    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",
      Arguments: { length: 0},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

multiply() 执行的时候会直接在 outer: <GlobalLexicalEnvironment>,中查找a,b

对外部环境的引用意味着它能够访问其外部词法环境。这意味着若是在当前词法环境中找不到它们,JavaScript 引擎能够在外部环境中查找变量。这就是以前说的 做用域链

闭包

MDN 解释 闭包是由函数以及建立该函数的词法环境组合而成。这个环境包含了这个闭包建立时所能访问的全部局部变量

这是我认为对闭包最合理的解释了,就看你怎么理解闭包的机制了。
其实闭包与做用域链有着密切的关系。

首先咱们来看看什么样的代码会产生闭包。

function foo() {
  var name = 'artiely'
  function bar() {
    console.log(`hello `)
  }
  bar()
}
foo()

以上代码是有闭包吗?没有~

function foo() {
  var name = 'artiely'
  function bar() {
    console.log(`hello ${name}`)
  }
  bar()
}
foo()

咱们只作了微小的调整,如今就有闭包了,咱们只是在bar中加入了name得引用
上面的代码还能够写成这样

// 或者
function foo() {
  var name = 'artiely'
  return function bar() {
    console.log(`hello ${name}`)
  }
}
foo()()

对于闭包的造成我进行了以下的几点概括

  1. A 函数内必须有 B 函数的声明;
  2. B 函数必须引用 A 函数的变量;
  3. B 函数被调用(固然前提是 A 函数被调用)

以上 3 点缺一不可

咱们来分析一下上面代码的执行上下文

// 建立
fooFunctionExectionContext = {
LexicalEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    Arguments: { length: 0},
    bar: < func >,
  },
  outer: <GlobalLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    name: undefined
  },
  outer: <GlobalLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>
  }
}
// 执行 略
// 建立
barFunctionExectionContext = {
LexicalEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
    Arguments: { length: 0},
  },
  outer: <fooLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
  EnvironmentRecord: {
    Type: "Declarative",
  },
  outer: <fooLexicalEnvironment>,
  ThisBinding: <Global Object or undefined>
  }
}
// 执行 略

这里由于bar的建立存在着对fooLexicalEnvironment里变量的引用,虽然foo可能执行已结束但变量不会被回收。这种机制被叫作闭包

闭包是由函数以及建立该函数的词法环境组合而成。这个环境包含了这个闭包建立时所能访问的全部局部变量

咱们结合上面例子从新分解一下这句话

闭包是由函数bar以及建立该函数foo的词法环境组合而成。这个环境包含了这个闭包建立时所能访问的全部局部变量name

可是从chrome的理解,闭包并无包含所能访问的全部局部变量,仅仅包含所被引用的变量。

this 绑定

在全局执行上下文中,值是 this 指全局对象。(在浏览器中,this 指的是 Window 对象)。

在函数执行上下文中,值 this 取决于函数的调用方式。若是它由对象引用调用,则将值 this 设置为该对象,不然,将值 this 设置为全局对象或 undefined(在严格模式下)。例如:

let person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear)
  }
}

person.calcAge()
// 'this' 指向 'person', 由于 'calcAge' 是被 'person' 对象引用调用的。

let calculateAge = person.calcAge
calculateAge()
// 'this' 指向全局 window 对象,由于没有给出任何对象引用

注意全部的()()自调用的函数 this 都是指向Global Object的既浏览器中的window

最后

若是本文对你有帮助或以为不错请帮忙点赞,若有疑问请留言。

其余参考:
https://hackernoon.com/execution-context-in-javascript-319dd72e8e2c

https://tylermcginnis.com/ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript/

https://hackernoon.com/javascript-execution-context-and-lexical-environment-explained-528351703922

相关文章
相关标签/搜索