声明
本系列仅为个人学习笔记,因此内容由网络整理以及学习课程而来,如果涉及到侵权,请联系我
介绍
本文是 JavaScript 高级深入浅出系列的第一篇,本文介绍了理解 JS 运行原理与作用域的部分知识,通过深入 JS 的编译原理来理解为什么 JS 中会有变量提升现象,相信你阅读了本文会有不一样的收获
正文
1. 为什么要学习 JS 高级
现在前端轮子越造越多,在提升生产力的同时,也让我叫苦不迭学不动了。万变不离其宗,任何框架都是对于基础 JS 的封装,只有掌握了原理,才会对于任何武功秘籍手到擒来。而想要熟练运用 JS 甚至理解 JS 的精髓,就要深入了解作用域、闭包、this 指向等知识。也正因为如此,你需要深入学习 JS 高级。
2. JavaScript 是如何运行在浏览器上的?
在了解此知识点之前,你需要先知道一个前置知识。
2.1 从浏览器输入网址敲击回车,到渲染页面出现,这中间发生了什么?
这是一个老生常谈的问题了,只要涉及到前端面试知识点,基本都会讲这个问题。这里不深入全面的讲,这里简单提一嘴这个前置知识。
- 输入网址(域名)
- DNS 服务器解析域名,解析到服务器的 IP 地址
- 如果没有指定端口,访问服务器的 80 端口,返回
index.html文件 - 浏览器解析
index.html文件,遇到 CSS 文件就下载并解析,遇到 JS 文件就下载并解析 - 浏览器的 UI 渲染内核渲染 CSS 与 HTML,JS 解析内核用于执行 JS 代码
- 最终展现到用户的浏览器上的,是渲染好的网页
这里要提一句,浏览器的内核一般是包括了 渲染内核与解析引擎,例如 webkit 内核,就由两部分组成:渲染内核 webcore 与 解析引擎 jscore
2.2 了解各大浏览器的 JS 引擎
JS 解析引擎的主要作用,就是将 JS 代码转换为机器码,再由 CPU 去执行。
几种常见的 JS 引擎:
- SpiderMonkey: Mozilla(火狐开发团队) 项目的一部分
- Chakra:IE 浏览器 JS 引擎
- JSCore:safari 浏览器 JS 引擎
- V8:chrome 浏览器 JS 引擎
本文就以 V8 为例,切入 JS 的解析过程
2.3 V8 引擎解析 JS 的过程
JS source code -> AST
- 源码通过 scanner 模块进行词法分析,将代码拆成一个一个的 token (词法单元),例如
let a = 2;,就被分割为let、a、=、2、;,当然,空格是否被分为 token ,则看此语言中空格是否有特殊意义。 - token 通过 Parser 和 PreParser 模块转为 AST
- parse 将 token 进行语法分析,在这个过程会进行合理校验,如果出现语法错误就会抛出异常。最终输出 AST
- preparser 称为预解析,因为不是所有的 JS 代码都需要立即执行,所以一些不必要的函数暂时预解析,只有在调用函数时才会进行全量解析
AST -> byte code
- V8 的 ignition 解释器会将 AST 解析为字节码,同时会在这个过程中收集 TurboFan 需要的一些信息
- 如果检测出此函数只运行一次,那么就会编译为字节码去执行
AST -> machine code
- 在 iginion 解释阶段,我们说到会为 TurboFan 收集信息,如果检测出某个函数被执行了多次,那么每一次被编译为字节码去执行肯定是不如机器码快的,所以会将一些热点函数编译为算法优化后的机器码
- 有时,机器码也会被逆向转为字节码,这是因为有时调用某个函数传入的参数类型不同,比如调用
hello('world')、与调用hello(123),虽然函数相同,但是由于参数的类型不同,这里执行机器码是会有问题的,所以需要转为字节码。
3. 深入 JS 执行过程
var foo = 'bar'
function greeting() {
console.log('hello world')
}
var num1 = 10
var num2 = 20
var result = num1 + num2
greeting()
3.1 初始化全局对象
JS 引擎在执行之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象所有的作用域都可以访问
- 里面会包含
Date、Math、setTimeout、setInterval、String、Number等。 - 里面会有一个变量
window指向自己,也就是 GO
ECMA Script 官方会这样描述:
Every execution context has an associated VariableEnvironment. Variables and functions decleared in ECMAScript code evaluated in an execution context are added as bindings in that VariableEnvironment's Environment Record. For function code, paramters are also added as bindings to that Environment Record.
每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。
对于函数来说,参数也会作为环境记录添加到变量函数中。
在 ES5 之前,变量环境 VE 是 variable object ,因此后文中 VE、VO 均指向 VE。
3.2 执行上下栈
JS 引擎内部有一个执行上下文栈(Execution Context Stack, ECS)是用于调用代码的执行栈,这个执行栈其实就是执行的全局的代码块
- 全局的代码块为了执行会创建一个 Global Execution Stack GEC,全局执行栈
- 全局执行栈会放在执行上下文栈中执行
GEC 放在 ECS 中包含两个部分:
- 在代码执行前:在 source code -> AST 的过程中,会将全局定义的变量加入到 GO 中,但是并不会赋值。这个过程也被称为变量提升
- 在代码执行中,对变量进行赋值,执行函数
3.3 遇到函数如何执行
在执行代码时如果遇到了函数,就会创建一个函数执行栈(Functional Execution Statck, FES),会被压到 ECS 中
FEC 包含三个方面的内容:
-
在解析函数到 AST 的过程中,会创建一个 Activation Object, AO:包含形参、arguments、函数定义和指向函数对象、定义的变量。
-
作用域链,由 VE(VariableEnvirnoment)(函数中就是 AO),由父级的 VO 组成,查找时会一层一层的找。
-
this 绑定的值,根据不同情况绑定 this
总结
本文中,你学习到了三个知识点:
1. V8 是如何解析 JS 的
通过 parse 转为 AST ,由 ignition 和 turboFan 转为字节码或机器码执行
2. JS 是如何执行的
执行前
会在堆内存中创建一个全局变量GO,其中有一个全局方法以及一个 window 指向自己
执行中
- 内部的执行上下文栈 ECS 为了执行全局代码会创建全局执行栈 GEC ,会把全局变量创建并压入 GO 中,但是并不赋值,在执行过程中,会对变量进行赋值,执行函数
- 遇到函数,会创建一个函数执行栈 FEC ,包含三个内容,VE、Scope Chain、this binding
为什么会有变量提升
因为在执行 JS 中内部的 GEC 全局执行栈 会将全局变量创建并压入 GO 全局对象中,但不赋值,所以变量访问可以存在变量创建前,只不过值是 undefined