JavaScript执行上下文与调用栈

先来看一段经典的题目:

function Fn() {
  getName = function () {
    console.log(1, this)
  }
  console.log(this)
  return this
}
Fn.getName = function () {
  console.log(2, this)
}
Fn.prototype.getName = function () {
  console.log(3, this)
}
var getName = function () {
  console.log(4, this)
}
function getName() {
  console.log(5, this)
}
// no.1
Fn.getName()
// no.2
getName()
// no.3
Fn().getName()
// no.4
getName()
// no.5
new Fn.getName()
// no.6
new Fn().getName()
// no.7
new new Fn().getName()

乍一看,打印的结果应该是:

// no.1 静态方法,打印 2, f Fn {...}
// no.2 最后一个覆盖全局,打印 5, Window {...}
// no.3 Fn执行打印 Window {...},再调用全局,打印 5, Window {...}
// no.4 再次执行全局,打印 5, window {...}
// no.5 new出实例,调用原型方法,打印 3, Fn {...}
// no.6 new出实例,调用原型方法,打印 3, Fn {...}
// no.7 new出实例,再new实例对象报错

但是,实际打印的结果是:

// no.1 打印 2, f Fn {...}
// no.2 打印 4, window {...}
// no.3 打印 window {...}, 1, window {...}
// no.4 打印 1, window {...}
// no.5 打印 2, Fn.getName {}
// no.6 打印 Fn {}, 3, Fn {}
// no.7 打印 Fn {}, 3, Fn.getName {}

先用断点调试探探路,究竟怎么个事儿?

getName1.jpg

no.1调用处打上断点,从全局可以看到Fn的静态方法getName和原型方法getName。这不难理解,所以no.1打印2Fn {}no.5no.6打印的结果应该都是3Fn {}才对,难道new Fnnew Fn()不同?

console.log(new Fn)  // Fn {}
console.log(new Fn())  // Fn {}

在没有参数的情况下,看上去没有太大不同,但为什么结果不同呢?在no.5no.6处打上断点,首先断点呈现出现差异:no.6new处和getName处出现两个可以标记的点。单步调试no.5Fn.getName进入调用堆栈,此时的this指向Fn.getName。继续单步调试no.6Fn进入调用堆栈,此时的this指向Fn。继续单步调试,Fn.prototype.getName进入调用堆栈,此时的this指向Fn

no.5处的newFn.getName()作为构造函数,而no.6处的newFn()作为构造函数。于是no.5打印2no.6打印3。假如no.5处不是函数而是属性,那么就会产生报错,所以在编写代码的时候应尽量加上()减少歧义来保证预期的效果。接着再看no.7,就可以得出正确的执行结果。在no.7处打上断点进行单步调试,先执行了new Fn(),然后执行了new Fn.prototype.getName()。因此先后打印Fn3Fn.getName,当然这里Fn.getNamethis指向Fn

getName2.jpg

再看看余下的调用情况,给它们加上断点。Fn.getName()执行完成,全局中的getNamevar所声明,于是no.2打印4Fn()执行完成,全局中的getName变为Fn()内部的声明。Fn()于返回this,也就是window,再调用getName()no.3打印1。再次执行getName,全局定义未变,no.4打印1

getName3.jpg

那么问题来了,function声明的getName为什么先后被varFn()的声明所覆盖呢?

执行上下文

context.png

简单地说,执行上下文是一个抽象的概念,是一个eval和执行JavaScript代码的环境。每当任何代码在JavaScript中运行时,它都在执行上下文中运行。

JavaScript中有三种类型的执行上下文:

  • 全局执行上下文(Global Execution Context,GEC):这是默认或基本执行上下文。不在任何函数内部的代码位于全局执行上下文中。它执行两件事:它创建了一个全局对象(Global Object,GO),它是一个窗口对象(在浏览器的情况下),并将 this 的值设置为等于全局对象。一个程序中只能有一个全局执行上下文。
  • 函数执行上下文(Functional Execution Context,FEC):每次调用函数时,都会为该函数创建一个全新的函数对象(Activation Object, AO)。每个函数都有自己的执行上下文,但它是在调用函数时创建的。可以有任意数量的函数执行上下文。每当创建一个新的执行上下文时,它都会按照定义的顺序执行一系列步骤。
  • Eval函数执行上下文(Eval Functional Execution Context,EFEC):eval函数是一个应该不惜一切代价避免的函数,每当JavaScript引擎遇到eval函数时,就会构造一个执行上下文并将其推入调用堆栈。由于eval的恶意性质,它不被推荐使用。

执行上下文的创建分两个阶段:1. 创建阶段,2. 执行阶段。

创建阶段

执行上下文是在创建阶段创建的,在创建阶段会发生以下事情:

词法环境(Lexical Environment)

词法环境组件是基于JavaScript代码的词法嵌套结构的结构,其定义标识符与变量和函数的值的关联。绑定是将标识符与变量和函数的值相关联的过程。

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

简单地说,词法环境是一个包含标识符-变量映射的结构。简单地说,词法环境是保存标识符-变量映射的结构。这里的标识符是指变量、函数的名称,变量是对实际对象(包括函数对象和数组对象)或原语值的引用。每个词法环境有三个组件:

  • 环境记录(Environment Record):环境记录是存储词法环境中变量和函数声明的地方。
    • 声明性环境记录(Declarative environment record):这主要由在函数执行上下文中创建的词法环境使用。它记录(存储)函数声明和变量声明。声明性环境记录还存储一个 arguments 对象,其中包含传递给函数的参数的长度(数量)以及参数及其索引的映射。
    • 对象环境记录(Object environment record):这主要由全局执行上下文中创建的词法环境使用。它存储函数声明、变量声明和全局绑定对象(浏览器中的窗口对象)。
    • 全局环境记录(Global Environment Record):虽然全局环境记录在理论上是单个记录,但它被声明为封装了对象和声明性环境记录的组合。它没有外部环境,因为它是[[OuterEnv]]null
  • 外部引用环境(Reference to the outer environment):对外部环境的引用意味着它可以访问其外部词法环境。这意味着JavaScript引擎可以在外部环境中寻找变量,如果它们在当前词法环境中找不到。
  • This绑定(This binding):在该组件中,确定或设置 this 的值。在全局执行上下文中,this的值是指全局对象。在函数执行上下文中,this的值取决于函数的调用方式。如果它被一个对象引用调用,那么this的值被设置为该对象,否则,this的值被设置为全局对象或undefined(在严格模式下)。
变量环境(Variable Environment)

变量环境组件是一个词法环境,它定义了标识符和变量值之间的关系,而不是函数。

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in memory>  
}

这两者之间的区别在于标识符在变量中是有界的。词法环境存储变量 letconst 和函数值的标识符绑定,而变量环境仅存储变量 var 值的标识符绑定。

执行阶段

在完成创建步骤之后,将进入执行阶段。JavaScript引擎再次扫描代码中的函数,用变量的值更新变量对象,然后运行代码,这被称为执行阶段。变量绑定初始化、变量赋值、可变性和不变性检查、变量绑定删除、函数调用执行等等都在此阶段发生。

回收阶段

在执行阶段之后,垃圾回收器将运行,并删除不再使用的变量绑定。

调用堆栈

函数调用形成若干帧组成的栈,在 JavaScript 中,调用栈(call stack)是用来存储函数执行的上下文的对象。当 JavaScript 引擎执行函数时,它会将当前函数的执行上下文压入调用栈中,当函数执行完毕后,它会将该函数的执行上下文从调用栈中弹出。

调用栈经常被用于存放子程序的返回地址。在调用任何子程序时,主程序都必须暂存子程序执行完毕后应该返回到的地址。因此,如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入调用栈,在其自身执行完毕后再行取回。在递归程序中,每一层次递归都必须在调用栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),调用栈就会产生栈溢出。

调用栈的主要功能是存放返回地址。除此之外,调用栈还用于存放:

  • 本地变量:子程序的变量可以存入调用栈,这样可以达到不同子程序间变量分离开的作用。
  • 参数传递:如果寄存器不足以容纳子程序的参数,可以在调用栈上存入参数。
  • 环境传递:有些语言(如Pascal与Ada)支持“多层子程序”,即子程序中可以利用主程序的本地变量。这些变量可以通过调用栈传入子程序。

当JavaScript引擎第一次遇到您的脚本时,它会创建一个全局执行上下文并将其推送到当前执行堆栈。每当引擎找到一个函数调用时,它就为该函数创建一个新的执行上下文,并将其推到堆栈的顶部。引擎执行其执行上下文位于堆栈顶部的函数。当此函数完成时,其执行堆栈将从堆栈中弹出,内存回收。执行将返回到前一个执行上下文,并继续执行下一个,执行完整个代码。

堆栈(stack)又称为栈或堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作,堆栈常用一维数组或链接串列来实现。常与另一种有序的线性资料集合队列相提并论。

堆栈的基本特点:

  • 先入后出,后入先出。
  • 除头尾节点之外,每个元素有一个前驱,一个后继。

堆栈是一种遵循后进先出(LIFO)原则的数据结构。然而,执行堆栈是跟踪在代码执行期间创建的所有执行上下文的堆栈。

  • 默认情况下,全局执行上下文最初以global()对象的形式添加(推送)到执行堆栈上。
  • 当调用或调用函数时,函数执行上下文被添加到堆栈中。
  • 被调用的函数被执行并从堆栈中删除(弹出),以及它的执行上下文。

总结

在创建阶段,JS引擎创建一个全局执行上下文来执行全局代码,它应该是这样的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Fn: <f Fn> {
        getName: <f>,
        arguments: <null>,
        caller: <null>,
        length: <length>,
        name: 'Fn',
        prototype: {
          getName: <f>,
          constructor: <f Fn>,
          [[Prototype]]: <Object>
        },
        [[FunctionLocation]]: <FunctionLocation String>,
        [[Prototype]]: <f>,
        [[Scopes]]: <Scopes Array>,
      }
    }
    outerEnv: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {     
      getName: <f> 
    },
    outerEnv: <null>,
    ThisBinding: <Global Object>
  }
}

在执行阶段,值被分配给变量。执行代码在var声明的getName之前,全局保持创建阶段function声明的函数体。在var声明的getName之后,所在的函数体赋值给全局。Fn()执行,getName作用域提升为全局,所在的函数体赋值给全局。因此全局getNameno.2打印4Fn()执行后no.3no.4打印1

全部评论

相关推荐

美团 后端开发 总包n(15%是股票)
点赞 评论 收藏
分享
10-28 11:04
已编辑
美团_后端实习生(实习员工)
一个2人:我说几个点吧,你的实习经历写的让人觉得毫无含金量,你没有挖掘你需求里的 亮点, 让人觉得你不仅打杂还摆烂。然后你的简历太长了🤣你这个实习经历看完,估计没几个人愿意接着看下去, sdk, 索引这种东西单拎出来说太顶真了兄弟,好好优化下简历吧
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务