关于执行上下文(作用域)、this的思考

1,203 阅读7分钟

image.png 只要往前多想点,你的故事就能更让人感同身受。

JavaScript编译原理(V8引擎)

几个重要概念和原理:

  • 编译器(Compiler)
  • 解释器(Interpreter)
  • 抽象语法树(AST)
  • 字节码(Bytecode)
  • 即时编译器(JIT)

编译器和解释器

编译型语言和解释型语言

之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言解释型语言
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

编译器执行过程

image.png

编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。

解释器执行过程

image.png

解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

V8引擎执行JavaScript代码过程

image.png

  • Ignition:V8引擎的解释器
  • TurboFan:V8引擎的编译器
  1. 生成抽象语法树(AST)和执行上下文

    将源代码转换为抽象语法树,并生成执行上下文。

    • 生成AST
      过程:
      • 第一阶段是分词(tokenize),又称为词法分析。
      • 第二阶段是解析(parse),又称为语法分析。
    • 生成该段代码的执行上下文 关于AST树生成及应用
  2. 生成字节码
    • 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
  3. 执行代码
    • 通常,如果有一段第一次执行的字节码,解释器Ignition会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
    • 关于V8引擎即时编译(JIT):解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

执行上下文(Execution Context,EC)

作用域

变量或函数的执行上下文决定了它们可以访问哪些数据,以及它们的行为。每个执行上下文都有一个关联的变量对象(variable object), 虽然这个执行上下文中定义的所有变量和函数都存在于这个对象上,但无法通过代码访问变量对象

作用域的分类

根据语言分类

  • 词法作用域:也称为静态作用域。(JavaScript采用)
  • 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等 词法作用域和动态作用域的区别其实在于划分作用域的时机:
  • 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
  • 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸。

词法作用域(静态作用域)

  1. 全局作用域(全局上下文)
  2. 函数作用域(函数上下文-函数定义时候就决定了
  3. 块级作用域(块级上下文)
  • 全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一 样。在浏览器中(非严格模式下),全局上下文就是我们常说的 window 对象,所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义 在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

    image.png

  • 函数上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的
  • 块级上下文 关联的变量对象(variable object)[[scope]更多]

作用域链

  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。
  • 代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链 的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
  • 增强作用域可以使用eval函数或with函数(不建议)

自由变量

自由变量在定义时候

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域(沿着作用域链),一层一层依次寻找,直到找到为止
  • 如果到全局作用域都没有找到,则报错 xxx is not defined

变量提升(Hositing)

  • var
  • function
  • 可以使用let、const避免

关于栈溢出(Stack Overflow)

关于this指向

普通函数(非箭头函数)

this 的指向是在调用时决定的,而不是在书写时决定的。

  1. 多数情况下,this 指向调用它所在方法的那个对象。说得更通俗点, 谁调的函数,this 就归谁。
  2. 当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined)

    特殊说明 三种特殊情境下,无论严格模式还是非严格模式,this会指向 window

    • 立即执行函数(IIFE)
    • setTimeout 中传入的函数
    • setInterval 中传入的函数

箭头函数

箭头函数中的 this,可以采用函数中自由变量的寻找值方式寻找this值。(作用域寻值方式)

改变this指向方法

  • bind
  • call
  • apply