前言
简单地说,JavaScript 引擎是一个解释 JavaScript 代码的计算机程序。它负责代码的执行。最流行的当数谷歌浏览器 Chrome 的V8引擎
,用于构建服务端程序的node.js就是以V8作为底层架构。今天就来谈谈浏览器执行JS代码的过程中都做了哪些事情?
在此之前我们需要了解,不同 ECMAScript 版本中对执行上下文的解释也有所不同:
ES3中,执行上下文包括:
- scope:作用域(作用域链)
- variable object:变量对象(存储变量的对象)
- this Binding:设置 this 关键字的值
在ES5中有所改动:
- lexical environment:词法环境
- variable environment:变量环境
- this Binding: 设置 this 关键字的值
此文基于 ES5 撰写。
正文
执行上下文
当浏览器解析到JS代码的时候,会发送给JS引擎来处理,JavaScript引擎会创建一个特殊的环境来处理这些JavaScript代码的转换和执行。这个特殊的环境就被称为执行上下文
。执行上下文包含了当前正在运行的代码以及有助于其执行的所有内容。
JS中执行上下文有三种:
- 全局执行上下文(GEC)
- 函数执行上下文(FEC)
- Eval 函数执行上下文
1. 全局执行上下文(GEC)
每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文,称为全局执行上下文,值得注意的是,每一个JavaScript文件只能有一个全局执行上下文
。他是最基础(默认)的执行上下文,所有不在函数内部的JavaScript代码都在这里执行。
2. 函数执行上下文(FEC)
每当函数被调用时
,JavaScript引擎就会创建另一种执行上下文,称为函数执行上下文(FEC)
,并且函数中的代码都会在函数执行上下文中进行评估和执行。由于每个函数调用都创建自己的FEC,所以在GEC中会存在n个FEC。
3. Eval 函数执行上下文
指的是运行在 eval
函数中的代码,也是有属于自己的执行上下文。但是不经常使用 eval
,鹅且不建议使用,在这里暂不讨论它。
执行上下文栈 (执行栈)
js是单线程的,一次只能同时做一件事,当存在多个上下文时,执行栈就负责管理这些上下文,它存储了代码执行期间所有的执行上下文。
执行栈是一种LIFO(后进先出)
的数据结构栈,用来存储代码运行时创建的所有执行上下文。当JavaScript引擎第一次遇到脚本时,他会创建一个全局的执行上下文并压入当前的执行栈。每当引擎遇到一个函数的调用,他会为该函数创建一个执行上下文并压入执行栈的顶部。引擎会执行那些执行上下文位于栈顶的函数,当函数执行完成时,执行上下文从栈顶弹出,控制流程到达当前栈中的下一个执行上下文。
js
let str = 'global execution context' // 1.全局上下文执行环境
function fn1() {
str = 'fn1() Function execution context'
console.log(str)
fn2() // 3. fn2函数上下文执行环境
}
function fn2() {
str = 'fn2() Function execution context'
console.log(str)
}
fn1() // 2. fn1函数上下文执行环境
str = 'global execution context' // 4. fn1执行完毕,如果有代码返回全局上下文,如果没有全局上下文出栈
console.log(str)
执行上下文是如何被创建的(生命周期)?
我们已经了解了不同种类的执行上下文及执行栈,现在让我们来看看执行上下文是如何被创建的,也就是执行上下文的生命周期。执行上下文的创建分为两个阶段:创建阶段,执行阶段,销毁阶段。
一. 创建阶段
在创建阶段,JavaScript引擎会扫描整个代码(不会执行 ),将代码中的变量声明 和函数声明 添加到变量对象VO 中。此过程中,执行上下文的变量对象 ,作用域 和作用域链 也会被创建,但是还没有具体的值。可以通俗的理解为:在创建阶段,JS引擎都是在对整个代码的变量和函数进行梳理,一共声明了哪些变量,定义了哪些函数。变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
执行上下文的创建阶段做了三件事:
· 创建词法环境(LexicalEnvironment)
· 创建变量环境(VariableEnvironment)
· 设置
this
关键字的值
1. 词法环境(LexicalEnvironment)
官方 这样定义词法环境(Lexical Environment):
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
译为:词法环境是一种规范类型(specification type),它基于 ECMAScript 代码的词法嵌套结构,来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
官方给出的解释很简单,但是很难理解,我们根据词法环境的组成来理解一下:
词法环境有两个组件 :
环境记录器
:存储 let, const, var 声明的变量 以及 函数声明 的实际位置;外部环境的引用(outer)
:一个外部引用,用来指向外部的执行上下文(父级作用域),由词法作用域指定
这里值得一提的是outer的引用,举个栗子:
js
function second() {
console.log('[output-second]', str)
}
function first() {
var str = "first "
second()
function third() {
console.log('[output-third]', str)
}
third()
}
var str = "outermost"
first()
// [output-second] outermost
// [output-third] first
图解:
third 的 outer 指向 first 执行上下文, first 的 outer 指向 window,变量先从当前执行作用域查找变量,如果找不到,就引着 outer 继续查找。整个过程中,third 作用域 ---> first 作用域 ---> window 作用域。
词法环境有两种类型:
全局环境
:是一个没有外部词法环境的词法环境,因此他的外部环境引用为null
。是一个全局对象,this
的值指向这个全局对象。函数环境
:在函数中定义的变量被存储在环境记录中,包含了arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
js
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境(是全局环境)
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符都绑定在这里
},
outer: <null> // 词法环境对外部的引用为null,意味着当前词法环境就是全局环境
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
Arguments: {length: 0},
// 标识符都绑定在这里
},
// 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
2. 变量环境(VariableEnvironment)
变量环境也是一个词法环境,因此它具有词法环境的所有属性。
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量(
let
,const
,class
)绑定,而后者仅用于存储变量(var
)绑定和 function 函数声明。
js
GlobalExectionContext = {
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 标识符都绑定在这里
},
outer: <null>
}
}
3. 代码示例 - 词法环境/变量环境
在搞明白全局环境与函数环境的概念后,我们结合代码,对词法环境进行进一步的理解:
js
const a = 10
let b
var c
function deelB(num1, num2) {
let d = num1 + num2
var e = num1 * num2
return d + e
}
b = deelB(10, 20)
// 以上代码完整的执行上下文就是:👇👇
// GlobalExectionContext = { // 全局执行上下文
// ThisBinding: <Global Object>,
// LexicalEnvironment: { // 词法环境
// EnvironmentRecord: { // 环境记录
// Type: "Object", // 全局
// // 标识符绑定在这里
// a: <uninitialized>,
// b: <uninitialized>,
// deelB: <func>
// },
// outer: <null>
// },
// VariableEnvironment: { // 变量环境
// EnvironmentRecord: { // 环境记录
// Type: "Object", // 全局
// // 标识符绑定在这里
// c: <undefined>,
// },
// outer: <null>
// }
// }
// deelB函数执行上下文
// FunctionExectionContext = { // 函数执行上下文
// ThisBinding: <Global Object>,
// LexicalEnvironment: { // 词法环境
// EnvironmentRecord: {
// Type: "Declarative", // 函数
// // 标识符绑定在这里
// Arguments: {0: 10, 1: 20, length: 2},
// d: <undefined>
// },
// outer: <deelB>
// },
// VariableEnvironment: { // 变量环境
// EnvironmentRecord: {
// Type: "Declarative", // 函数
// // 标识符绑定在这里
// e: <undefined>
// },
// outer: <deelB>
// }
// }
4. 设置 this
关键字的值
this在不同上下文中,指向的内容也会不同:
- 全局执行上下文中,this 指向全局对象,即 window 对象;
- 函数执行上下文中,this 的指向取决于函数的调用方式(默认绑定、隐式绑定、显式绑定/硬绑定、new 绑定、箭头函数)
换句话说,函数的调用方式决定了 this
的值。在创建阶段,this的值并不能确定下来,只有在执行的时候才能确认。this的值主要取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this
会被设置成那个对象,否则 this
的值被设置为全局对象或者 undefined
(在严格模式下)。
二. 执行阶段
执行阶段中,JS 代码开始逐条执行,在这个阶段主要做的事情有:
- 完成对变量的赋值(let, const, var)
- 函数引用
- 执行其他代码
三. 销毁阶段
当函数执行完成后,当前执行上下文会从执行上下文栈中出栈,等待被回收,控制权被重新交给执行栈上一层的执行上下文。
注意:这只是一般情况,闭包是一种特殊情况!
闭包我们可以理解为:定义在一个函数内部的函数,其中内部函数在包含他的外部函数之外被调用,就会形成闭包。
js
function fn() {
const str = 'fn'
return function() {
console.log(str)
}
}
const logFn = fn()
logFn()
当闭包的父函数fn执行完成后,父函数本身执行环境的作用域链会被销毁,但是由于闭包的作用域链仍然在引用父函数的变量对象,导致了父函数的变量对象会一直驻存于内存,无法销毁,除非闭包的引用被销毁,闭包不再引用父函数的变量对象,这块内存才能被释放掉。