掘金第一文:深入V8引擎:JavaScript执行机制与作用域机制

7 阅读6分钟

一、JavaScript 引擎到底是什么?

我们日常写的 JS 代码,本身只是一堆纯文本字符,电脑根本看不懂。JS 引擎就是专门负责读懂、运行 JavaScript 代码的程序

目前全世界主流能跑 JS 的运行环境,一共就两大类:

  1. 浏览器环境:Chrome、Edge、火狐这些浏览器里自带的 JS 运行环境
  2. Node 环境:可以脱离浏览器,在电脑后台、服务器上单独跑 JS 的环境

而这两个环境能运行 JS 的核心共同点:底层都内置了 Google 开发的 V8 引擎

什么是 V8 引擎?

V8 是谷歌用 C++ 语言开发的开源高性能 JS & WebAssembly 引擎。你可以把它理解成 JS 的「通用翻译官」:不管是浏览器里的网页,还是 Node 后台程序,所有 JS 代码最终都要交给 V8,由它翻译成电脑能直接执行的指令。它本质就是一个超大型的功能函数,核心使命就是读懂文本格式的 JS 代码,并且执行代码逻辑


二、JS 代码完整执行流程

很多人误以为:代码写好,V8 拿到就直接从上往下跑了。大错特错! V8 拿到 JS 文本代码后,不会立刻执行,要先走完一整套「编译梳理流程」,全部处理完之后,才会真正运行代码。完整分为 3 步:

  1. 词法分析(分词) 最简单理解:把一长串完整的代码文本,从左到右「拆零件」。把整段代码拆成一个个最小、不可再拆分的基础单元(关键字、变量名、符号、数字等),就像把一整句话拆成单个的汉字、词语、标点,是引擎解析代码的第一步。
  2. 语法分析(解析) 在上一步拆分好的词语基础上,按照 JavaScript 官方语法规则,把零散的单元拼装成抽象语法树(AST) 。这一步会校验代码语法对不对,识别代码里所有的变量、函数、标识符,检查有没有语法写错的地方。
  3. 生成代码 把上面生成好的语法树,最终转换成电脑 CPU 能直接识别、运行的机器指令。到这一步编译流程全部结束,代码才正式开始执行。

三、JS 函数的本质

我们写的 function foo() {} 就是标准的函数结构。函数的核心意义:代码逻辑的「打包容器」

你可以把函数理解成一个「功能盒子」:我们把一段需要重复使用、有独立逻辑的代码,全部打包封进这个盒子里。盒子里的代码,默认不会自动运行。只有当你主动调用这个函数(写 foo())的时候,盒子才会打开,里面包裹的代码才会依次执行。

举个直白例子:函数就像你手机里的 App,代码写好了只是安装完毕,你点开 App(调用函数),里面的功能才会运行;你不点开,它就安安静静待着,不会自己跑。


四、JS 重中之重:作用域机制

作用域,通俗翻译就是:变量的「可访问范围」。它规定了:你写的变量、函数,在代码的哪些地方能被读取、使用,哪些地方访问不到。JavaScript 里一共只有 3 种作用域,层层嵌套、边界分明:

1. 全局作用域

它是整个 JS 程序最外层、最大的公共空间。所有没写在任何函数、代码块 {} 里面的代码、变量,全部都归属全局作用域。特点:全局作用域里的内容,代码全处任何地方都能访问,没有访问限制,是整个程序的「公共广场」。如下图所示

abec0878956c19e6aaba6c940727910a.png

2. 函数作用域

function 函数大括号 {} 包裹起来的独立私密空间,就连函数的传入参数,也属于这个函数作用域。特点:内部定义的变量,只在函数里面能用,函数外面完全访问不到。函数就是自带一层「隐私围墙」,里面的东西不会泄露到外面,外层也闯不进内层拿变量。如下图所示

7bb7a6fc850cc091acfa1e42dc623222.png

3. 块级作用域

{} 代码块(比如 iffor 循环的大括号)形成的独立空间。
⚠️ 关键限定:只有 letconst 声明的变量,才会拥有块级作用域;老式的 var 变量完全无视块级作用域。大括号内部用 let/const 声明的变量,只在这个 {} 里面有效,括号外面访问不到。如下图所示

f4e66e8046f42702b27e0d49a8f7dfc4.png


作用域的查找规则

外层作用域是不能访问内层作用域的,作用域只能由内往外查找,在当前的作用域中找不到变量时,会向上一级的作用域查找,直到全局作用域

补充:暂时性死区

只要一个 {} 块级作用域里,用了 let / const 声明变量,这个变量就直接绑定当前代码块。在代码块内、变量声明语句之前的所有位置,都属于这个变量的暂时性死区。哪怕外部全局有同名变量,在这里也完全访问不到,强行提前访问直接报错。简单说:块内声明了 let 变量,这块区域就彻底和外部同名变量「隔绝」了。如下图所示

3d9320f08e5ae3dcc4f78d7644288a67.png


JS 中 关于变量var、let、const 的全部区别

1. 重复声明的权限不同

var 的宽松度极高,在同一个作用域内,可以对同一个变量重复多次声明,程序不会报错,后续的声明会直接覆盖掉前面的内容。

letconst 有着严格的语法限制,同一个作用域里面,绝对不允许重复声明同一个变量,只要重复声明,代码就会直接报错。

2. 变量数值的修改权限不同

varlet 声明的都是普通变量,在完成声明和初次赋值之后,后续代码里可以随时随地修改变量内部存储的值

const 专门用来声明常量,在完成第一次声明并且赋值之后,这个变量的值就被彻底锁死,后续永远不允许再修改

3. 各自管辖的作用域范围不同

我们先明确 JS 的三类作用域:全局作用域、函数作用域、{} 代码块形成的块级作用域。

  • var 的管辖范围很窄:只认可全局作用域、函数作用域,完全无视块级作用域,代码块的大括号根本限制不住 var 声明的变量。
  • letconst 的管辖范围完整全面:全局、函数、块级三类作用域全部支持{} 代码块可以完美限制住这两种变量,实现变量的私有化隔离。

4. 变量提升与暂时性死区的区别

首先纠正一个新手误区:varletconst 三者全部都会发生变量提升,三者的区别不在于有没有提升,而在于提升之后的访问规则。

  • var 完成变量提升后,会自动初始化一个默认值 undefined。因此在变量声明语句之前,提前访问这个变量不会报错,只会拿到默认值 undefined
  • letconst 虽然同样会发生变量提升,但是会被暂时性死区全程锁定。在当前作用域内,变量声明语句执行之前的所有区域都属于死区,只要提前访问变量,代码就会直接报错