🚀 别再死记硬背变量提升了!一文搞懂 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 执行机制的自然结果。