前端面试之原理部分

JavaScript的运行机制

本节你需要搞清楚以下问题:

  • 作用域链本质上是如何产生的?
  • this是如何绑定的?
  • JavaScript代码运行原理是什么?
  • 闭包产生的原因?

JavaScript的执行环境

在了解JavaScript运行机制之前,我们需要搞清楚几个主要概念。

JavaScript引擎(JavaScript Engine)

目前市面上的JavaScript引擎:

  • V8 — 开源,由 Google 开发,用 C ++ 编写
  • Rhino — 由 Mozilla 基金会管理,开源,完全用 Java 开发
  • SpiderMonkey — 是第一个支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用
  • JavaScriptCore — 开源,以Nitro形式销售,由苹果为Safari开发
  • KJS — KDE 的引擎,最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发
  • Chakra (JScript9) — Internet Explorer
  • Chakra (JavaScript) — Microsoft Edge
  • Nashorn, 作为 OpenJDK 的一部分,由 Oracle Java 语言和工具组编写
  • JerryScript —  物联网的轻量级引擎


V8引擎由两个主要部件组成:
emory Heap(内存堆) — 内存分配地址的地方
Call Stack(调用堆栈) — 代码执行的地方

JavaScript运行时(JavaScript Runtime)

想让JavaScript真正运作起来,单单靠JavaScript Engine是不够的,JavaScript Engine的工作是编译并执行 JavaScript 代码,完成内存分配、垃圾回收等,但是缺乏与外部交互的能力。

比如单靠一个V8引擎是无法进行ajax请求、设置定时器、响应事件等操作的,这就需要JavaScript运行时(JavaScript Runtime)的帮助,它为 JavaScript 提供一些对象或机制,使它能够与外界交互。

比如,虽然Chrome和node都是用了V8引擎,但是他们的运行时却不同,比如process、fs浏览器都无法提供。

可执行代码

一段js代码的运行可以分为2个阶段:

  • 编辑阶段
    • 分词/词法分析(Token)
    • 解析/语法分析(Parsing)
    • 预编译(解释)
  • 执行阶段
    js并非简单的一行行解释执行,而是将js代码分为一块块的可执行代码块进行执行,那么如何划分代码块?
    目前有三大类代码块:
    • 函数代码块(Function code)
    • 全局代码块(Global code)
    • eval代码块(Eval code)

JavaScript执行

先看一个简单的例子:

1
2
3
4
5
var name = 'cxk'

function say(name) {
return `${name} like singing dancing and rap`
}

思考一下,javascript是如何执行它的呢?

上面的代码声明被存放在中。如图:

此时虽然变量和函数都被声明了,但是函数还没执行,我们现在执行say函数

1
2
3
4
5
6
7
var name = 'cxk'

function say(name) {
return `${name} like singing dancing and rap`
}

say(name)

调用栈

我们声明的函数与变量被储存在『内存堆』中,而当我们要执行的时候,就必须借助于『调用栈』来解决问题。如图:

那么是不是将函数压入栈内就结束了?肯定没有这么简单,这里需 要在引入一个概念,执行上下文(execution context)。

执行上下文(execution context)

执行上下文在代码块执行前创建,作为代码块的基本执行环境,那么执行上下文分为几种?
前面我们提到过,JavaScript中有三种可执行代码块,当然也对应着三种执行上下文。

  • 全局执行上下文 —— 这是基础上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 —— 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建。
  • Eval执行上下文 —— 执行在 eval 内部的代码也会有它属于自己的执行上下文,除非你想搞黑魔法,不然不要轻易使用它。

那么,这个执行上下文到底包含哪些东西呢?如何运作的呢?
执行上下文分为两个阶段:

  • 创建阶段
  • 执行阶段

我们主要讨论创建阶段,执行阶段的主要工作就是分配变量。

执行上下文的创建阶段

执行上下文的创建阶段主要解决以下三点:

  • 决定this的指向
  • 创建词法环境(LexicalEnvironment)
  • 创建变量环境(VariableEnvironment)

伪代码如下:

1
2
3
4
5
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvirnoment = {...},
VariableEnvirnoment = {...}
}

this指向

this的指向是在代码执行阶段确定的,所谓的「代码执行阶段」正是「执行上下文的创建阶段」。
默认情况下this指向全局对象,还有隐士绑定、显示绑定、new调用3种规则。

词法环境(LexicalEnvironment)

词法环境分为三大类:

  • 全局环境:全局环境的外部环境引用是null,它拥有内建的Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window对象)还有任何用户定义的全局变量,并且this的值指向全局对象。
  • 模块环境:包含模块顶级声明的绑定以及模块显式导入的绑定。 模块环境的外部环境是全局环境。
  • 函数环境:函数内部用户定义的变量存储在环境记录器中,外部引用既可以是其它函数的内部词法环境,也可以是全局词法环境。

词法环境本身包括两个部分:

  • 『环境记录器(Environment Record)』是存储变量和函数声明的实际位置
  • 『外部环境的引用(outer Lexical Environment)』指它可以访问其父级词法环境(即作用域)

对于『环境记录器』而言,它又分为两个主要的环境记录器类型:

  • 声明式环境记录器(DecarativeEnvironmentRecord):范围包含函数定义,变量声明,try…catch等,
    此类型对应起范围内包含的声明定义的标识符集
  • 对象式环境记录器(ObjectEnvironmentRecord):由程序级别的(Program)对象、声明、with语句等创建,与称为其绑定对象的对象相关联,此类型对应于其绑定对象的属性名称的字符串标识符名称集

比如我们在全局声明一个函数:

1
2
3
function add(a,b) {
return a + b
}

那么他的词法环境可以这样标表示(下图我们省略了this绑定、变量环境等信息,便于理解):

变量环境(VariableEnvironment)

变量环境的定义在es5标准和es6标准是略有不同的,我们采用es6的标准。

变量环境也是一个词法环境,但不同的是词法环境被用来存储函数声明和变量(let 和 const)绑定,而变量环境只用来存储 var 变量绑定。

执行过程

我们就先把本节开头的例子再拓展一下:

1
2
3
4
5
6
7
8
9
10
11
var name = 'cxk' 

function say(name) {
const bgm = 'ji ni tai mei'
function play() {
const content = `${name} like singing dancing and rap, ${bgm}`
return content
}
}

const content = say{name}

我们就一步步复盘一下上述代码是如何执行的(不考虑解析、预解释等操作,只考虑执行):

1. 变量name和函数声明say被存放在堆中

2. 创建全局可执行上下文

全局上下文的伪代码如下:

示意图:

3. 创建函数执行上下文

say函数的执行上下文伪代码如下:

4. 创建say函数体内的函数执行上下文

play函数的执行上下文伪代码如下:

示意图如下:

5. 开始执行

将上下文中的变量赋值,然后执行代码,执行完栈顶的play函数后弹出,接着执行say函数,完毕后弹出。

小结

我们通过本文了解了相关的JavaScript执行机制,现在可以回答这几个问题了。

  1. this是怎么被绑定的?
    创建可执行上下文的时候,根据代码的执行条件,来判断分别进行默认绑定、隐式绑定、显示绑定等。

  2. 作用域链是怎么形成的?
    可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成链作用域链。

  3. 闭包是怎么形成的?
    可执行上下文中的词法环境含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等。

事件循环(Event Loop)

一次弄懂Event Loop

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!