首页»JavaScript»解密JavaScript执行上下文

解密JavaScript执行上下文

来源:玩弄心里的鬼 发布时间:2019-05-22 阅读次数:

 执行上下文栈

  首先我们先了解一下什么是执行上下文栈(Execution context stack)。

  上面这张图来自于mdn,分别展示了栈、堆和队列,其中栈就是我们所说的执行上下文栈;堆是用于存储对象这种复杂类型,我们复制对象的地址引用就是这个堆内存的地址;队列就是异步队列,用于event loop的执行。

  JS代码在引擎中是以“一段一段”的方式来分析执行的,而并非一行一行来分析执行。而这“一段一段”的可执行代码无非为三种:Global code、Function Code、Eval code。这些可执行代码在执行的时候又会创建一个一个的执行上下文(Execution context)。例如,当执行到一个函数的时候,JS引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文。

  那么随着我们的执行上下文数量的增加,JS引擎又如何去管理这些执行上下文呢?这时便有了执行上下文栈。

  这里我用一段贯穿全文的例子来讲解执行上下文栈的执行过程:

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

  当JS引擎去解析代码的时候,最先碰到的就是Global code,所以一开始初始化的时候便会将全局上下文推入执行上下文栈,并且只有在整个应用程序执行完毕的时候,全局上下文才会推出执行上下文栈。

  这里我们用ECS来模拟执行上下文栈,用globalContext来表示全局上下文:

ESC = [
   globalContext, // 一开始只有全局上下文
]

  然后当代码执行checkscope函数的时候,会创建checkscope函数的执行上下文,并将其压入执行上下文栈:

ESC = [
  checkscopeContext, // checkscopeContext入栈
  globalContext,
]

  接着代码执行到return f()的时候,f函数的执行上下文被创建:

ESC = [
  fContext, // fContext入栈
  checkscopeContext,
  globalContext,
]

  f函数执行完毕后,f函数的执行上下文出栈,随后checkscope函数执行完毕,checkscope函数的执行上下文出栈:

// fContext出栈
ESC = [
  // fContext出栈
  checkscopeContext,
  globalContext,
]
// checkscopeContext出栈
ESC = [
  // checkscopeContext出栈
  globalContext,
]

 变量对象

  每一个执行上下文都有三个重要的属性:

  • 变量对象
  • 作用域链
  • this

  这一节我们先来说一下变量对象(Variable object,这里简称VO)。

  变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。并且不同的执行上下文也有着不同的变量对象,这里分为全局上下文中的变量对象和函数执行上下文中的变量对象。

  全局上下文中的变量对象

  全局上下文中的变量对象其实就是全局对象。我们可以通过this来访问全局对象,并且在浏览器环境中,this === window;在node环境中,this === global。

  函数上下文中的变量对象

  在函数上下文中的变量对象,我们用活动对象来表示(activation object,这里简称AO),为什么称其为活动对象呢,因为只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。

  在函数执行之前,会为当前函数创建执行上下文,并且在此时,会创建变量对象:

  • 根据函数arguments属性初始化arguments对象;
  • 根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针。如果函数名称已存在,则覆盖;
  • 根据变量声明生成对应的属性,此时初始值为undefined。如果变量名已声明,则忽略该变量声明;

  还是以刚才的代码为例:

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

  在执行checkscope函数之前,会为其创建执行上下文,并初始化变量对象,此时的变量对象为:

VO = {
  arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 传入的参数
  f: pointer to function f(),
  scope: undefined, // 此时声明的变量为undefined
}

  随着checkscope函数的执行,变量对象被激活,变相对象内的属性随着代码的执行而改变:

VO = {
  arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 传入的参数
  f: pointer to function f(),
  scope: 'local scope', // 变量赋值
}

  其实也可以用另一个概念“函数提升”和“变量提升”来解释:

function checkscope(s) {
  function f() { // 函数提升
    return scope;
  }
  var scope; // 变量声明提升	
  scope = 'local scope' // 变量对象的激活也相当于此时的变量赋值
  return f();
}

 作用域链

  每一个执行上下文都有三个重要的属性:

  • 变量对象
  • 作用域链
  • this

  这一节我们说一下作用域链。

  什么是作用域链

  当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  下面还是用我们的例子来讲解作用域链:

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

  首先在checkscope函数声明的时候,内部会绑定一个[[scope]]的内部属性:

checkscope.[[scope]] = [
  globalContext.VO
];

  接着在checkscope函数执行之前,创建执行上下文checkscopeContext,并推入执行上下文栈:

  • 复制函数的[[scope]]属性初始化作用域链;
  • 创建变量对象;
  • 将变量对象压入作用域链的最顶端;
     
// -> 初始化作用域链;
checkscopeContext = {
  scope: checkscope.[[scope]],
}

// -> 创建变量对象
checkscopeContext = {
  scope: checkscope.[[scope]],
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 传入的参数
    f: pointer to function f(),
    scope: undefined, // 此时声明的变量为undefined
  },
}

// -> 将变量对象压入作用域链的最顶端
checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO = {
    arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 传入的参数
  f: pointer to function f(),
  scope: undefined, // 此时声明的变量为undefined
  },
}

  接着,随着函数的执行,修改变量对象:

checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 传入的参数 
    f: pointer to function f(),
    scope: 'local scope', // 变量赋值
  }
}

  与此同时遇到f函数声明,f函数绑定[[scope]]属性:

checkscope.[[scope]] = [
  checkscopeContext.VO, // f函数的作用域还包括checkscope的变量对象
  globalContext.VO
];

  之后f函数的步骤同checkscope函数。

  再来一个经典的例子:

var data = [];	
for (var i = 0; i < 6; i++) {
  data[i] = function () {
    console.log(i); 
  }; 
}
data[0]();
// ...

  很简单,不管访问data几,最终console打印出来的都是6,因为在ES6之前,JS都没有块级作用域的概念,for循环内的代码都在全局作用域下。

  在data函数执行之前,此时全局上下文的变量对象为:

globalContext.VO = { 
  data: [pointer to function ()], 
  i: 6, // 注意:此时的i值为6 
}

  每一个data匿名函数的执行上下文链大致都如下:

data[n]Context = {
  scope: [VO, globalContext.VO],
  VO: {
    arguments: {
      length: 0,
    }
  }
}

  那么在函数执行的时候,会先去自己匿名函数的变量对象上找i的值,发现没有后会沿着作用域链查找,找到了全局执行上下文的变量对象,而此时全局执行上下文的变量对象中的i为6,所以每一次都打印的是6了。

  词法作用域 & 动态作用域

  JavaScript这门语言是基于词法作用域来创建作用域的,也就是说一个函数的作用域在函数声明的时候就已经确定了,而不是函数执行的时候。

  改一下之前的例子:

var scope = 'global scope';
function f() {
  console.log(scope)
}
function checkscope() {
  var scope = 'local scope';
  f();
}
checkscope();

  因为JavaScript是基于词法作用域创建作用域的,所以打印的结果是global scope而不是local scope。我们结合上面的作用域链来分析一下:

  首先遇到了f函数的声明,此时为其绑定[[scope]]属性:

// 这里就是我们所说的“一个函数的作用域在函数声明的时候就已经确定了”
f.[[scope]] = [
  globalContext.VO, // 此时的全局上下文的变量对象中保存着scope = 'global scope';
];

  然后我们直接跳过checkscope的执行上下文的创建和执行的过程,直接来到f函数的执行上。此时在函数执行之前初始化f函数的执行上下文:

// 这里就是为什么会打印global scope
fContext = {
  scope: [VO, globalContext.VO], // 复制f.[[scope]],f.[[scope]]只有全局执行上下文的变量对象
  VO = {
    arguments: {
      length: 0,
    },
  },
}

  然后到了f函数执行的过程,console.log(scope),会沿着f函数的作用域链查找scope变量,先是去自己执行上下文的变量对象中查找,没有找到,然后去global执行上下文的变量对象上查找,此时scope的值为global scope。

 this

  在这里this绑定也可以分为全局执行上下文和函数执行上下文:

  • 在全局执行上下文中,this的指向全局对象。(在浏览器中,this引用 Window 对象)。
  • 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)

  总结起来就是,谁调用了,this就指向谁。

 执行上下文

  这里,根据之前的例子来完整的走一遍执行上下文的流程:

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

  首先,执行全局代码,创建全局执行上下文,并且全局执行上下文进入执行上下文栈:

globalContext = {
  scope: [globalContext.VO],
  VO: global,
  this: globalContext.VO
}
ESC = [
  globalContext,
]

  然后随着代码的执行,走到了checkscope函数声明的阶段,此时绑定[[scope]]属性:

checkscope.[[scope]] = [
  globalContext.VO,
]

  在checkscope函数执行之前,创建checkscope函数的执行上下文,并且checkscope执行上下文入栈:

// 创建执行上下文
checkscopeContext = {
  scope: [VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 传入的参数
    f: pointer to function f(),
    scope: undefined,
  },
  this: globalContext.VO,
}

// 进入执行上下文栈
ESC = [
  checkscopeContext,
  globalContext,
]

  checkscope函数执行,更新变量对象:

// 创建执行上下文
checkscopeContext = {
  scope: [VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 传入的参数
    f: pointer to function f(),
    scope: 'local scope', // 更新变量
  },
  this: globalContext.VO,
}

  f函数声明,绑定[[scope]]属性:

f.[[scope]] = [
  checkscopeContext.VO,
  globalContext.VO,
]

  f函数执行,创建执行上下文,推入执行上下文栈:

// 创建执行上下文
fContext = {
  scope: [VO, checkscopeContext.VO, globalContext.VO], // 复制[[scope]]属性,然后VO推入作用域链顶端
  VO = {
    arguments: {
      length: 0,
    },
  },
  this: globalContext.VO,
}
	
// 入栈
ESC = [
  fContext,
  checkscopeContext,
  globalContext,
]

  f函数执行完成,f函数执行上下文出栈,checkscope函数执行完成,checkscope函数出栈:

ESC = [
  // fContext出栈
  checkscopeContext,
  globalContext,
]

ESC = [
  // checkscopeContext出栈,
  globalContext,
]

  到此,一个整体的执行上下文的流程就分析完了。

QQ群:凯发娱乐官网官方群(515171538),验证消息:10000
微信群:加小编微信 849023636 邀请您加入,验证消息:10000
提示:更多精彩内容关注微信公众号:全栈开发者中心(fsder-com)
188bet uedbet 威廉希尔 明升 bwin 明升88 bodog bwin 明升m88.com 18luck 188bet unibet unibet Ladbrokes Ladbrokes casino m88明升 明升 明升 m88.com 188bet m88 明陞 uedbet赫塔菲官网 365bet官网 m88 help
网友评论(共0条评论) 正在载入评论......
理智评论文明上网,拒绝恶意谩骂 发表评论 / 共0条评论
登录会员中心