别再死记硬背变量提升了!一文搞懂 JavaScript 执行原理

0 阅读4分钟

🚀 别再死记硬背变量提升了!一文搞懂 JavaScript 执行原理

最近在学习《你不知道的 JavaScript》和 JavaScript 执行机制相关内容,发现以前很多记住的知识点其实只是现象,并没有理解背后的运行逻辑。今天整理一下自己的学习笔记,希望帮助和我一样刚入门的同学建立正确认知。


前言

刚开始学习 JavaScript 的时候,经常会遇到一些看起来很奇怪的代码。

比如:

console.log(a);

var a = 10;

输出结果:

undefined

再比如:

showName();

function showName() {
    console.log("Hello JavaScript");
}

居然还能正常运行。

按照代码从上到下执行的逻辑来看,这些代码似乎都应该报错。

那么问题来了:

JavaScript 真的是一行一行执行的吗?

答案是:

是,但不完全是。


JavaScript 执行流程

很多人以为 JS 引擎拿到代码后会直接开始执行:

第一行
↓
第二行
↓
第三行

实际上在执行之前,还会经历一个准备阶段。

整个过程大致如下:

JavaScript源码

↓

编译阶段

↓

执行阶段

也就是说:

JavaScript 并不是拿到代码立即执行,而是先进行编译,再开始运行。


什么是变量提升(Hoisting)

最经典的例子:

console.log(name);

var name = "Tom";

输出:

undefined

很多教程会解释成:

var name;

console.log(name);

name = "Tom";

这种理解方式方便记忆,但并不准确。

实际上:

代码的位置从来没有发生改变。

真正发生的是:

在编译阶段,JS 引擎会提前收集变量声明。

类似于:

Variable Environment

name -> undefined

所以执行到:

console.log(name);

时变量已经存在。

只是值还是:

undefined

为什么函数可以提前调用?

代码:

showName();

function showName() {
    console.log("Hello");
}

执行结果:

Hello

原因在于:

函数声明在编译阶段就已经创建完成。

showName
    ↓

function showName(){}

因此真正执行时:

showName();

能够直接找到对应的函数对象。


函数表达式为什么不行?

很多同学第一次都会踩这个坑。

showName();

var showName = function () {
    console.log("Hello");
}

结果:

TypeError:
showName is not a function

原因是:

编译阶段只提升变量声明。

showName -> undefined

而赋值:

= function(){}

仍然留在执行阶段。

所以调用时实际相当于:

undefined();

自然报错。


JavaScript 为什么需要执行上下文?

当代码开始运行时,JS 引擎会创建一个运行环境。

这个环境就是:

Execution Context(执行上下文)

例如:

var name = "Tom";

function test() {
    var age = 20;
}

全局执行时:

Global Execution Context

name -> Tom

test -> function

调用:

test();

后又会创建新的执行上下文:

Function Execution Context

age -> 20

函数执行结束后,该上下文会被销毁。


调用栈(Call Stack)

来看一个例子:

function a() {
    b();
}

function b() {
    c();
}

function c() {
    console.log("Hello");
}

a();

执行过程:

a()
 ↓
b()
 ↓
c()

对应调用栈:

顶部
────────
c
────────
b
────────
a
────────
底部

执行结束后:

c 出栈

↓

b 出栈

↓

a 出栈

这也是为什么浏览器报错时会显示:

Call Stack

什么是词法环境(Lexical Environment)

学习 ES6 之后,经常会听到一个概念:

词法环境

其实没那么神秘。

可以简单理解为:

当前作用域中的变量仓库。

例如:

let age = 20;

const sex = "男";

存储形式类似:

Lexical Environment

age -> 20

sex ->

为什么 let 和 const 会报错?

代码:

console.log(age);

let age = 20;

结果:

ReferenceError

很多人会说:

let 没有变量提升。

其实这种说法并不严谨。

准确来说:

let 和 const 同样会被创建。

只是创建后处于:

<uninitialized>

未初始化状态。

而不是:

undefined

因此在声明之前访问:

console.log(age);

就会直接报错。


暂时性死区(TDZ)

例如:

{
    console.log(age);

    let age = 20;
}

从进入作用域开始:

{
    ...
}

变量已经存在。

但尚未初始化:

age -> <uninitialized>

从进入作用域到执行:

let age = 20;

这段区域就叫:

TDZ(Temporal Dead Zone)

暂时性死区。

在这里访问变量:

age

就会触发:

ReferenceError

V8 引擎到底做了什么?

Chrome 浏览器中的 JavaScript 主要由 V8 引擎执行。

整体流程如下:

JavaScript源码

↓

词法分析(Lexer)

↓

生成AST抽象语法树

↓

编译

↓

创建执行上下文

↓

创建变量环境

↓

创建词法环境

↓

执行代码

这也是变量提升、作用域链、闭包等知识的基础。


我的理解

以前学习变量提升时,我只是记住:

var 可以先使用
let 不可以
function 可以提前调用

但总感觉这些知识点是零散的。

直到开始学习 JavaScript 执行原理后才发现:

变量提升只是结果。

真正重要的是理解:

  • 编译阶段
  • 执行阶段
  • 执行上下文
  • 变量环境
  • 词法环境
  • 调用栈

当这些概念串起来之后,很多以前觉得奇怪的现象都会变得非常合理。


总结

一句话概括:

var 会在编译阶段创建并初始化为 undefined;function 会直接创建函数对象;let 和 const 同样会被创建,但会进入暂时性死区,只有执行到声明语句时才会完成初始化。

理解这一点之后,变量提升就不再是死记硬背的知识点,而是 JavaScript 执行机制的自然结果。