资讯专栏INFORMATION COLUMN

解密JavaScript执行上下文

JeOam / 689人阅读

摘要:执行上下文栈首先我们先了解一下什么是执行上下文栈。那么随着我们的执行上下文数量的增加,引擎又如何去管理这些执行上下文呢这时便有了执行上下文栈。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

执行上下文栈

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


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

JS代码在引擎中是以“一段一段”的方式来分析执行的,而并非一行一行来分析执行。而这“一段一段”的可执行代码无非为三种:Global codeFunction CodeEval 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,
]

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

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

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

相关文章

  • 解密 JavaScript 执行下文

    摘要:闭包就好像从中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能到达那里。兴奋地赶紧自测咔咔咔连点三下。结果当时内心表情大概就像上面这个哥们。但还是在工位上故作镇定地赶紧百度了下。   闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能到达那里。——《你不知道的JavaScript 上卷》 1、起源 js闭包很长一...

    khlbat 评论0 收藏0
  • 基础知识 - 收藏集 - 掘金

    摘要:本文是面向前端小白的,大手子可以跳过,写的不好之处多多分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 JavaScript字符串所有API全解密 - 掘金关于 我的博客:louis blog SF专栏:路易斯前端深度课 原文链接:JavaScript字符串所有API全解密 本文近 6k 字,读完需 10 分钟。 字符串作为基本的信息交流的桥梁,几乎被所有的编程...

    wdzgege 评论0 收藏0
  • 当我们在谈论前端加密时,我们在谈些什么

    摘要:所以我们今天只谈前端加密,一个部分人认为没有意义的工作。在中,认证过程使用了非对称加密算法,非认证过程中使用了对称加密算法。非对称加密上文中我们讨论了前端的哈希加密以及应用的场景。 showImg(https://segmentfault.com/img/bVAhTC); 当然在谈安全。 前端安全是Web安全的一部分,常见的安全问题会有XSS、CSRF、SQL注入等,然而这些已经在程师...

    wizChen 评论0 收藏0
  • 系统潜入后门分析

    摘要:在这个案例里,这些是欺骗性的功能,它们似乎有一个唯一目的,即混淆自动检测系统,反病毒软件,或者那些甚至尝试手工分析这些程序样本的分析人员。受害机器的处于所规定的地址空间,攻击者是无法通过到达的。 初始传染手段 - Nuclear Pack 已经有一些其他的文章介绍过Nuclear Pack破解工具包。可能它还不像g10pack或者BlackHole这些工具那么流行,也没有像CoolE...

    forrest23 评论0 收藏0
  • 【译】JavaScript中的执行下文和堆栈是什么?

    摘要:每次调用函数时,都会创建一个新的执行上下文。理解执行上下文和堆栈可以让您了解代码为什么要计算您最初没有预料到的不同值的原因。 首发:https://www.love85g.com/?p=1723 在这篇文章中,我将深入研究JavaScript最基本的部分之一,即执行上下文。在这篇文章的最后,您应该更清楚地了解解释器要做什么,为什么在声明一些函数/变量之前可以使用它们,以及它们的值是如何...

    miguel.jiang 评论0 收藏0

发表评论

0条评论

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