JS 编译执行过程
编译型语言与解释型语言
- 编译型语言:在代码执行前,预编译代码转换成机器语言,每次执行时不用重新编译。编译器生成的目标程序是面向特定平台。
- 解释型语言:不预编译代码,在执行代码前先解释转换成机器语言再执行,每次执行时都需要先逐行解释再逐行运行。解释器自己执行源程序,不依赖于某一平台。
- 解释型语言的执行速度要慢于编译型语言,但跨平台性好。
比如:最初的 C 和 C++ 都是编译型语言,而最初的 JavaScript 是解释型语言。编译器启动速度慢,执行速度快。而解释器的启动速度快,执行速度慢。不过随着即时编译(Just-in-time compilation,JIT),一种混合使用编译器和解释器的技术的发展,大部分语言都可以即时编译了。
现在应尽量避免将编程语言划分为编译型或解释型语言。 详见知乎提问
为了提高 JS 的执行效率,浏览器厂商都在不断努力。目前性能最高的 JS 引擎是 V8 引擎,它引入了 Java 虚拟机和 C++ 编译器的众多技术,实现了即时编译,V8 引擎属于 JIT 编译器。
JS 编译执行过程
- 编译过程:
Parser
解析器编译 JS 代码,词法分析 → 语法分析 → 生成AST
→ 确定作用域 → 生成字节码, - 执行过程:
Ignition
解释器解释执行编译过程生成的字节码。
词法分析生成许多个词法单元 tokens
let message = 'hi'
Tokens
[
{
type: 'Keyword',
value: 'let'
},
{
type: 'Identifier',
value: 'message'
},
{
type: 'Punctuator',
value: '='
},
{
type: 'String',
value: "'hi'"
}
]
AST
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "message"
},
"init": {
"type": "Literal",
"value": "hi",
"raw": "'hi'"
}
}
],
"kind": "let"
}
],
"sourceType": "script"
}
执行上下文
执行上下文栈( Execution Context Stack,ECStack),又称执行栈、调用栈。将上下文压入栈中执行,执行完毕则将上下文弹出栈。
执行上下文有三种:
- 全局执行上下文(Global Execution Context,GEC):代码运行会首先进入该环境
- 函数执行上下文(Function Execution Context,FEC):当函数被调用执行时,会进入当前函数中执行代码
- eval(不建议使用)
执行上下文栈底永远都是全局上下文,而栈顶就是当前执行的上下文。
ES5 变量对象
执行上下文中的变量对象(Variable Object,VO)保存了当前上下文中声明的变量和函数
JS 引擎在执行代码之前,先创建一个全局对象 Global Object(GO)。全局对象可以被所有作用域访问,全局执行上下文中的变量对象就是全局对象
函数执行上下文中:
- 在分析阶段,函数执行上下文中的变量对象初始化当前上下文中声明的变量和函数;
- 在执行阶段,函数接收传入的参数并对函数内声明的变量进行赋值,变量对象被激活成活动对象(Activation Object,AO)
活动对象与变量对象的关系:
- 活动对象 AO = 变量对象 VO + 形式参数 + 实际参数列表 arguments
- 变量对象 VO 保存使用
var
声明的变量和function
声明的函数
ES5 执行上下文伪代码:
ExecutionContext = {
ThisBinding = <this value>, // 确定 this
ScopeChain = { 当前 VO, 父级 VO }, // 作用域链
VariableObject = { 当前 VO 使用 var 声明的变量和 function 声明的函数, 形参, 实参列表 }, // 变量对象
}
ES6 词法环境
ES6 相较于 ES5,新增了词法环境,并将变量对象 VO 改为变量环境 VE
词法环境(Lexical Environment)包含了一个环境记录(Environment Record)和一个指向外部词法环境的引用,而这个引用的值可能为null
。
ES6 执行上下文伪代码:
ExecutionContext = {
ThisBinding = <this value>, // 确定 this
LexicalEnvironment = { 环境记录(即当前 LE), outer(即父级 LE) }, // 词法环境
VariableEnvironment = { 当前 LE 使用 var 声明的变量, outer(即父级 LE)}, // 变量环境
}
词法环境与变量环境的区别:
- 词法环境用于保存当前上下文中使用
let
、const
、class
、module
、import
声明的标识符引用和function
声明的函数 - 变量环境用于保存使用
var
声明的标识符引用
词法环境有三种类型:
- 全局词法环境:全局执行上下文中,没有引用外部环境的词法环境
- 函数词法环境:函数执行上下文中的词法环境
- 变量环境:变量环境也是一种词法环境
变量提升
var
关键字声明的变量初始化被提前,赋值未被提前,如下:
console.log(n) // undefined
var n = 1
// 变量提升后等价于下面代码
var n
console.log(n) // undefined
a = 1
函数提升
创建函数有两种方式:函数声明、函数表达式(又称函数字面量)
function
关键字声明的函数初始化被提前,只有函数声明才存在函数提升,如下:
console.log(f1) // function f1() {}
console.log(f2) // undefined
function f1() {}
var f2 = function () {}
// 等价于
function f1() {}
var f2
console.log(f1) // function f1() {}
console.log(f2) // undefined
f2 = function () {}
同名标识符,函数提升先于变量提升:
console.log(foo) // function foo() {}
var foo = 1
function foo() {}
var foo = 2
顶层对象与全局变量
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。
ES6 为了改变这一缺陷,同时为了兼容性,规定:
var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。
var a = 1
this.a // 1
window.a // 1
let b = 1
window.b // undefined
例题
例一:在函数外声明变量,在函数中可以访问并修改该变量值
var n = 100
function foo() {
n = 200
}
foo()
console.log(n) // 200
例二:函数声明中的变量提升,变量提升初始化到函数自己的作用域中(易错)
// 作用域层级:foo({n: undefined -> 200}) -> Global({n: 100})
function foo() {
console.log(n) // undefined
var n = 200
console.log(n) // 200
}
var n = 100
foo()
例三:函数嵌套调用函数
var n = 100
// 作用域层级:foo1({n: 100}) -> Global({n: 100})
function foo1() {
console.log(n) // 2: 100
}
// 作用域层级:foo2({n: 200}) -> Global({n: 100})
function foo2() {
var n = 200
console.log(n) // 1: 200
foo1()
}
foo2()
// 作用域层级:Global({n: 100})
console.log(n) // 3: 100
例四:尽管 return 后的代码无法执行,但在编译阶段,return 后的代码中所有的变量声明和函数声明都会初始化到 AO。
var n = 100
// 作用域层级:foo({n: undefined}) -> Global({n: 100})
function foo() {
console.log(n) // undefined
return
var n = 200
}
foo()
如下是 return 前调用 return 后声明的函数,:
var n = 100
// 作用域层级:foo({n: undefined}) -> Global({n: 100})
function foo() {
console.log(n) // undefined
bar()
return
var n = 200
// 作用域层级:bar -> foo({n: undefined}) -> Global({n: 100})
function bar() {
console.log(n) // undefined
}
}
foo()
但是 return 后的函数声明中的变量声明无法初始化,如下:
var n = 100
function foo() {
console.log(n) // undefined
bar()
console.log(m) // 报错:m 未定义
return
var n = 200
function bar() {
var m = 'm'
console.log(n) // undefined
}
}
foo()
例五:在函数中使用 var
、let
、const
关键字声明的变量不会成为全局对象的属性
// 作用域层级:foo({n: 100}) -> Global
function foo() {
var n = 100
}
foo()
console.log(n) // 报错:n 未定义
然而,在函数中不使用 var
关键字声明变量:
function foo() {
// 去掉 var
n = 100
}
foo()
console.log(n) // 100
console.log(window) // 可以发现 window 上有 n
不使用关键字声明变量(只初始化却不声明)如 n = 100
,在严格意义上是错误语法,但 JS 引擎会把这个变量放到全局对象上。
使用关键字同时声明初始化变量,如 var/let/const a = b = 100
等价于 var/let/const a = 100; b = 100
:
function foo() {
var a = b = 100
// 等价于下面两行代码
// var a = 100
// b = 100
let c = d = 200
}
foo()
console.log(b) // 100
console.log(d) // 200
// console.log(a) // 报错,a 未定义
// console.log(c) // 报错,c 未定义