JavaScript 之深刻理解执行上下文

在 JavaScript 中,执行上下文是一个基本的概念,但其中又包含了变量对象、做用域链、this 指向等更深刻的内容,深刻理解执行上下文以及其中的内容,对咱们之后理解 JavaScript 中其它更深刻的知识点(函数/变量提高、闭包等)会有很大的帮助。前端

执行上下文(Execution Context)

执行上下文能够理解为当前代码的运行环境。在 JavaScript 中,运行环境主要包含了全局环境函数环境es6

在 JavaScript 代码运行过程当中,最早进入的是全局环境,而在函数被调用时则进入相应的函数环境。全局环境和函数环境所对应的执行上下文咱们分别称为全局上下文函数上下文express

在一个 JavaScript 文件中,常常会有多个函数被调用,也就是说在 JavaScript 代码运行过程当中极可能会产生多个执行上下文,那么如何去管理这多个执行上下文呢?数组

执行上下文是以栈(一种 LIFO 的数据结构)的方式被存放起来的,咱们称之为执行上下文栈(Execution Context Stack)浏览器

在 JavaScript 代码开始执行时,首先进入全局环境,此时全局上下文被建立并入栈,以后当调用函数时则进入相应的函数环境,此时相应函数上下文被建立并入栈,当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。bash

因此在执行上下文栈中,栈底永远是全局上下文,而栈顶则是当前正在执行的函数上下文。数据结构

文字表达既枯燥又难以理解,让咱们来看一个简单的栗子吧~闭包

function fn2() {
  console.log('fn2')
}
function fn1() {
  console.log('fn1')
  fn2();
}
fn1();
复制代码

运行上述代码,能够获得相应的输出,那么上述代码在执行过程当中执行上下文栈的行为是怎样的呢?函数

/* 伪代码 以数组来表示执行上下文栈 ECStack=[] */
// 代码执行时最早进入全局环境,全局上下文被建立并入栈
ECStack.push(global_EC);
// fn1 被调用,fn1 函数上下文被建立并入栈
ECStack.push(fn1_EC);
// fn1 中调用 fn2,fn2 函数上下文被建立并入栈
ECStack.push(fn2_EC);
// fn2 执行完毕,fn2 函数上下文出栈
ECStack.pop();
// fn1 执行完毕,fn1 函数上下文出栈
ECStack.pop();
// 代码执行完毕,全局上下文出栈
ECStack.pop();
复制代码

以一个更形象的图来讲明上述的流程post

执行上下文栈 ECStack

在一个执行上下文中,最重要的三个属性分别是变量对象(Variable Object)、**做用域链(Scope Chain)**和 this 指向

咱们能够采用以下方式表示

EC = {
  VO,
  SC,
  this
}
复制代码

一个执行上下文的生命周期分为建立执行阶段。建立阶段主要工做是生成变量对象创建做用域链肯定 this 指向。而执行阶段主要工做是变量赋值以及执行其它代码等。

变量对象(Variable Object)

咱们已经知道,在执行上下文的建立阶段会生成变量对象,生成变量对象主要有如下三个过程:

  1. 检索当前上下文中的参数。该过程生成 Arguments 对象,并创建以形参变量名为属性名,形参变量值为属性值的属性;
  2. 检索当前上下文中的函数声明。该过程创建以函数名为属性名,函数所在内存地址引用为属性值的属性;
  3. 检索当前上下文中的变量声明。该过程创建以变量名为属性名,undefined 为属性值的属性(若是变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性)。

咱们能够经过如下伪代码来表示变量对象

VO = {
  Arguments: {}, 
  ParamVariable: 具体值,  //形参变量
  Function: <function reference>, Variable:undefined } 复制代码

当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object)。此时原先声明的变量会被赋值。

变量对象和活动对象都是指同一个对象,只是处于执行上下文的不一样阶段

咱们能够经过如下伪代码来表示活动对象

AO = {
  Arguments: {},
  ParamVariable: 具体值,  //形参变量
  Function: <function reference>, Variable:具体值 } 复制代码

一样的,让咱们以实际栗子来理解在代码执行过程当中某执行上下文中变量对象的变化状况~

function fn1(a) {
  var b = 1;
  function fn2() {}
  var c = function () {};
}
fn1(0);
复制代码

当 fn1 函数被调用时,fn1 执行上下文被建立(建立阶段)并入栈,其变量对象以下所示

fn1_EC = {
  VO = {
    Arguments: {
      '0': 0,
      length: 1
    },
    a: 0,
    b: undefined,
    fn2: <function fn2 reference>, c:undefined } } 复制代码

而在 fn1 函数代码的执行过程当中(执行阶段),变量对象变为活动对象,原先声明的变量会被赋值,其活动对象以下所示

fn1_EC = {
  AO = {
    Arguments: {
      '0': 0,
      length: 1
    },
    a: 0,
    b: 1,
    fn2: <function fn2 reference>,
    c:<function express c reference>,
  }
}
复制代码

对于全局上下文来讲,因为其不会有参数传递,因此在生成变量对象的过程当中只有检索当前上下文中的函数声明和检索当前上下文中的变量声明两个步骤。

在浏览器环境中,全局上下文中的变量对象(全局对象)即咱们熟悉的 window 对象,经过该对象可使用其预约义的变量和函数,在全局环境中所声明的变量和函数,也会成为全局对象的属性。

弄明白了变量对象的生成过程后,咱们就可以更深刻地理解函数提高以及变量提高的内在机制了。

console.log(a) // undefined
fn(0); // fn
var a = 0;
function fn() {
  console.log('fn')
}
复制代码

上述代码中,在全局上下文的建立阶段,会检索上下文中的函数声明以及变量声明,函数会被赋值具体的引用地址而变量会被赋值为 undefined。

因此上述代码实际上的运行过程以下

function fn() {
  console.log('fn')
}
var a = undefined;
console.log(a) // undefined
fn(0); // fn
a = 0;
复制代码

因此,这就是咱们常常提到的函数提高以及变量提高的内在机制。

做用域链(Scope Chain)

做用域链是指由当前上下文和上层上下文的一系列变量对象组成的层级链。它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

咱们已经知道,执行上下文分为建立和执行两个阶段,在执行上下文的执行阶段,当须要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,如果没有找到,则会沿着上层上下文的变量对象进行查找,直到全局上下文中的变量对象(全局对象)。

那么当前上下文是如何有序地去查找它所须要的变量或函数的呢?

答案就是依靠当前上下文中的做用域链,其包含了当前上下文和上层上下文中的变量对象,以便其一层一层地去查找其所须要的变量和函数。

执行上下文中的做用域链又是怎么创建的呢?

咱们都知道,JavaScript 中主要包含了全局做用域和函数做用域,而函数做用域是在函数被声明的时候肯定的

每个函数都会包含一个 [[scope]] 内部属性,在函数被声明的时候,该函数的 [[scope]] 属性会保存其上层上下文的变量对象,造成包含上层上下文变量对象的层级链。[[scope]] 属性的值是在函数被声明的时候肯定的

当函数被调用的时候,其执行上下文会被建立并入栈。在建立阶段生成其变量对象后,会将该变量对象添加到做用域链的顶端并将 [[scope]] 添加进该做用域链中。而在执行阶段,变量对象会变为活动对象,其相应属性会被赋值。

因此,做用域链是由当前上下文变量对象及上层上下文变量对象组成的

SC = AO + [[scope]]
复制代码

让咱们来看个栗子~

var a = 1;
function fn1() {
  var b = 1;
  function fn2() {
    var c = 1;
  }
  fn2();
}
fn1();
复制代码

在 fn1 函数上下文中,fn2 函数被声明,因此

fn2.[[scope]]=[fn1_EC.VO, globalObj]
复制代码

当 fn2 被调用的时候,其执行上下文被建立并入栈,此时会将生成的变量对象添加进做用域链的顶端,而且将 [[scope]] 添加进做用域链

fn2_EC.SC=[fn2_EC.VO].concat(fn2.[[scope]])
=>
fn2_EC.SC=[fn2_EC.VO, fn1_EC.VO, globalObj]
复制代码

this 指向

**this 的指向,是在函数被调用的时候肯定的。**也就是执行上下文被建立时肯定的。

关于 this 的指向,其实最主要的是三种场景,分别是全局上下文中 this函数中 this构造函数中 this

全局上下文中 this

在全局上下文中,this 指代全局对象。

// 在浏览器环境中,全局对象是 window 对象:
console.log(this === window); // true
a = 1;
this.b = 2;
console.log(window.a); // 1
console.log(window.b); // 2
console.log(b); // 2
复制代码

函数中 this

函数中的 this 指向是怎样一种状况呢?

若是被调用的函数,被某一个对象所拥有,那么其内部的 this 指向该对象;若是该函数被独立调用,那么其内部的 this 指向 undefined(非严格模式下指向 window)。

举个栗子~

var a = 1;
function fn() {
  console.log(this.a)
}
var obj = {
  a: 2,
  fn: fn
}
obj.fn(); // 2
fn(); // 1
复制代码

上述代码中 fn 函数都是输出 this.a,根据上述的结论,obj.fn() 因为其是被 obj 对象所拥有,因此 this 指向 obj 对象;而 fn 是被独立调用,在非严格模式下 this 指向 window。

构造函数中 this

要清楚构造函数中 this 的指向,则必须先了解经过 new 操做符调用构造函数时所经历的阶段。

经过 new 操做符调用构造函数时所经历的阶段以下:

  1. 建立一个新对象;
  2. 将构造函数的 this 指向这个新对象;
  3. 执行构造函数内部代码;
  4. 返回这个新对象。

因此从上述流程可知,对于构造函数来讲,其内部 this 指向新建立的对象实例

function Person(name, age) {
  this.name = name;
  this.age = age;
}
var ttsy = new Person('ttsy', 24);
console.log(ttsy.name);  // ttsy
console.log(ttsy.age);  // 24
复制代码

须要注意的是,在 ES6 中箭头函数中,this 是在函数声明的时候肯定的,具体可看 es6.ruanyifeng.com/#docs/funct…

一个完整的栗子

接下来,让咱们来完整地 look 一下程序运行过程当中执行上下文及其内部属性的变化状况。

function fn1() {
  var a = 1;
  function fn2(b) {
    var c = 3
  }
  fn2(2)
}
fn1();
复制代码

上述代码在执行过程当中,执行上下文栈的变化过程以下

/* 伪代码 以数组来表示执行上下文栈 ECStack=[] */
// 代码执行时最早进入全局环境,全局上下文被建立并入栈
ECStack.push(global_EC);
// fn1 被调用,fn1 函数上下文被建立并入栈
ECStack.push(fn1_EC);
// fn1 中调用 fn2,fn2 函数上下文被建立并入栈
ECStack.push(fn2_EC);
// fn2 执行完毕,fn2 函数上下文出栈
ECStack.pop();
// fn1 执行完毕,fn1 函数上下文出栈
ECStack.pop();
// 代码执行完毕,全局上下文出栈
ECStack.pop();
复制代码

首先进入全局环境,全局上下文被建立并入栈

全局上下文以下

global_EC = {
  VO: globalObj,
  SC: [globalObj],
  this: globalObj,
}
复制代码

接着 fn1 被调用,fn1 函数上下文被建立并入栈

在 fn1 函数上下文被建立以前,会有一个函数声明过程,这个过程发生在全局上下文建立阶段,在这个过程当中,fn1.[[scope]] 会保存其上层做用域的变量对象。

在 fn1 函数上下文建立阶段,其执行上下文以下

fn1_EC = {
  VO: {
    Arguments: {
      length: 0
    },
    fn2: <function fn2 reference>, a:undefined }, SC:[fn1_EC.VO, globalObj], this:null } 复制代码

在 fn1 函数上下文执行阶段,其执行上下文以下

fn1_EC = {
  VO: {
    Arguments: {
      length: 0
    },
    fn2: <function fn2 reference>, a:1 }, SC:[fn1_EC.VO, globalObj], this:globalObj } 复制代码

而后在 fn1 中调用 fn2,fn2 函数上下文被建立并入栈

在 fn2 函数上下文建立阶段,其执行上下文以下

fn2_EC = {
  VO: {
    Arguments: {
      '0': 2,
      length: 0
    },
    b: 2,
    c: undefined
  },
  SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
  this: null
}
复制代码

在 fn2 函数上下文执行阶段,其执行上下文以下

fn2_EC = {
  VO: {
    Arguments: {
      '0': 2,
      length: 0
    },
    b: 2,
    c: 3
  },
  SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
  this: globalObj
}
复制代码

最后是各个上下文出栈

在各个上下文出栈后,其对应的变量对象会被 JavaScript 中的自动垃圾收集机制回收。

而咱们常常说闭包可以访问其所在环境的变量,实际上是由于闭包可以阻止上述变量对象被回收的过程。

深刻地理解了执行上下文的内容后,对于咱们理解闭包也会有很大的帮助,关于闭包我写过一篇 《 JavaScript 闭包详解 》,感兴趣的童鞋也能够继续阅读。


公众号不定时分享我的在前端方面的学习经验,欢迎关注。

相关文章
相关标签/搜索