JS特性之Hoisting(提升)
前言
为了写这篇文章其实也不容易,本来想讲GC的,发现lkq写过了,后来想讲类型转换,发现🗻🐟🏠写过了。
🫡🫡🫡
最后没办法了,让我水一水喽(bushi)
补一下,现在又学习了些新知识,发现这篇讲的有些错误,以补充或纠正的形式特此改正。(24/11/27)
Hoisting提升🏗️前置知识
啥是提升?
简单的说 Hoisting 是JS引擎在代码执行前将所有声明提升到其所在作用域的顶部
这句话你可以在看完这篇文章后再回头看看这句话,真的是一言蔽之。
接下来我会简单讲解一下。
所有声明
JS中拢共有
- 变常量
var let const
- 函数
function
- 类
class
- 模块
import export
是的这些都会提升,但是JS引擎在处理这些声明时会有不同的细节处理。
所在作用域
JS中拢共 global function block module
四个常规的作用域,这不会区别自己学去。
[[JS作用域]]
好像还有词法作用域 (Lexical Scope)、动态作用域 (运行时上下文context)、私有作用域 (Private Scope),嘿🫠我也不会,估计是不同的领域了,下次学到的时候补上😁😁😁。
补充
lexical scope 主要是和闭包closure有关系的概念,我在[[JS精进之Closure (闭包)]]这篇文章中会讲到
context运行时上下文,我目前的理解是包含了lexical scope(此外还有变量环境variable environment、this绑定),lexical是静态的,context是函数运行时的上下文,和函数栈有关,是动态的。
即context会根据lexical产生的词法作用域链、变量环境的信息、this绑定的信息形成具体的运行上下文
私有作用域目前我没学到🫠
类class有关的相关知识我也没完整的学习过,就不在这里乱讲了。
记住Hoisting提升只会提升到其所在作用域的顶部,不会发生将function scope内的提升到global scope的事情。
详细讲解
var
的提升
用 var
声明的变量会被提升到作用域顶部,但仅提升声明部分,初始化(赋值)不会提升。
提一句var会全局污染,即会绑定到window上,不了解的可以自己去查资料看var、let、const
的区别.
例子:
1 | console.log(a); // undefined |
底层执行过程:
1. JavaScript 引擎将 var a
提升到作用域顶部,等同于:
1
2
3
4
var a;
console.log(a); // undefined
a = 5;
console.log(a); // 5
2. 因此,在变量赋值之前,a
的值是 undefined
。
1 | var a; |
[[let 和 const 的提升]]
let
和 const
也会被提升,但它们的变量在提升时会被放入一个称为 暂时性死区(Temporal Dead Zone, TDZ) 的区域,只有在声明之后才能访问。
例子:
1 | console.log(b); // ReferenceError: Cannot access 'b' before initialization |
底层执行过程:
let b
被提升到作用域顶部,但在声明之前无法访问。- 由于在
let b
之前尝试访问变量b
,引擎抛出ReferenceError
。
啥是[[暂时性死区(Temporal Dead Zone, TDZ)]]
暂时性死区是 JavaScript 中一种行为:
在作用域内,虽然变量已经“被提升”(即解析时已经被声明),但在实际声明和初始化完成之前,访问该变量会抛出 ReferenceError
错误。
这是为了避免变量在声明之前被意外访问或使用,增加代码的可预测性和安全性。
有死区和没死区粗暴的看就是,一个爆ReferenceError
,一个爆undefined
函数的 Hoisting
函数又和前面的机制不同,函数声明的提升会将整个函数提升🫡🫡🫡,而函数表达式则只是会将变量提升,不提升函数。
#### 1. 函数声明的提升
⭐函数声明会被完整地提升到作用域顶部,因此在声明之前也可以调用函数。
例子:
1
2
3
4
greet(); // Hello!
function greet() {
console.log("Hello!");
}
底层执行过程:
1. 函数声明 function greet() { ... }
被完整提升到作用域顶部,等同于:
1
2
3
4
function greet() {
console.log("Hello!");
}
greet(); // Hello!
1 | greet(); // Hello! |
1 | function greet() { |
2. 函数表达式的提升
函数表达式(包括箭头函数)不会提升其赋值部分,仅变量名会被提升。
例子:
1 | console.log(foo); // undefined |
底层执行过程:
- 只有
var foo
被提升,赋值部分不会提升,等同于:1
2
3
4
5
6var foo;
console.log(foo); // undefined
foo = function () {
console.log("Hello!");
};
foo(); // Hello!
使用 let
或 const
声明函数表达式时,会触发暂时性死区:
1 | console.log(bar); // ReferenceError |
class
(类声明)
类声明也会被提升,但同样会存在暂时性死区,在声明之前无法访问。
示例:
1 | const instance = new MyClass(); // ReferenceError报错,MyClass 在 TDZ 中 |
import
(模块声明)
import
总是在模块顶部执行,不能在代码块中使用。
示例:
1 | import { myFunc } from './module.js'; |
export
(模块声明)
特点:
- 用于导出变量、函数、类等,使其可以在其他模块中使用。
- 支持命名导出(export {}
)和默认导出(export default
)。
示例:
1
2
3
4
export const myVar = 42;
export default function myFunc() {
console.log("Hello!");
}
1 | export const myVar = 42; |
对比总结:
特性 | var |
let |
const |
function |
class |
import/export |
---|---|---|---|---|---|---|
作用域 | \ | \ | \ | 函数作用域 | 块作用域 | 模块作用域 |
变量提升 | 是(值为 undefined ) |
是(TDZ限制) | 是(TDZ限制) | 是 | 是(TDZ限制) | 是 |
重复声明 | 可以 | 不可以 | 不适用 | 不适用 | 不适用 | |
可重新赋值 | 是 | 是 | 否 | 不适用 | 不适用 | 否 |
[[Hoisting提升的先后顺序]]
看到这不知道你有没有疑惑,所有声明都会发生提升,那究竟谁比谁更能提升🫡🫡🫡,总不能左脚🦶踩右脚🦶升天不是。
在 JavaScript 中,不同类型的声明(如 var
、let
、const
、function
、class
、import/export
)它们的执行优先级和提升行为有所不同。以下是提升的先后顺序及具体规则:
提升顺序的原则
- 模块系统优先:
import
和export
的静态绑定首先被处理。 - 函数声明优先:在代码执行前,函数声明会被提升到所在作用域的顶部。
- 变量声明依次提升:
var
早于let
和const
,但值初始化按代码顺序进行。 class
的提升特殊:类声明会被提升,但无法在声明前使用(存在 TDZ)。
提升优先级
类型 | 提升顺序 | 初始化可用性 | 是否受 TDZ 限制 | 特性解释 |
---|---|---|---|---|
import |
最高 | 在代码执行前绑定 | 是 | 模块加载的静态绑定,解析阶段完成,但无法访问 |
function |
第二 | 在声明前可调用 | 否 | 函数声明整体提升,初始化为可调用函数 |
var |
第三 | 在声明前可用(undefined ) |
否 | 声明提升但初始化在原始位置 |
let 和 const |
第四 | 声明前不可用 | 是 | 存在 TDZ,初始化需等到执行到声明语句才有效 |
class |
最后 | 声明前不可用 | 是 | 类声明会提升,但使用前需显式定义 |
- 最高优先级:
import/export
静态绑定最先解析。 - 中间优先级:
function
声明高于var
。 - 最低优先级:
let
、const
和class
,它们都受 TDZ 限制。
综合示例:优先级的对比
以下代码展示了多种声明的提升顺序和执行规则:
1 | console.log(func()); // "I am a function!" |
执行顺序:
- 函数
func
提升并初始化。 var x
声明提升但值为undefined
。let y
和const z
提升但进入 TDZ,未初始化。class MyClass
提升但进入 TDZ,未初始化。
为什么要提升?
这也是我当初了解到提升这一逆天特性时的最大疑惑,吃饱了撑着么要这么干?
我自己也解释不清,欧克,下面让我们看看GPT是怎么说的:
提升(Hoisting)是 JavaScript 的设计特性之一,它从一开始就融入了语言的实现中。设计提升的原因和历史背景与 JavaScript 的初衷、执行模型、开发环境的需求,以及语言设计的权衡密切相关。
- 简化解析和作用域解析,提升性能。
- 支持灵活的代码结构,适合快速开发和调试。
- 向下兼容,保留早期代码的行为一致性。
- 模仿 C 语言的声明模型,降低学习成本。
- 宽容开发者错误,降低入门门槛。
我们来挑重点看,主要就是俩点,一是能提高编译器效率,二是设计者就是想要易用🫡(毕竟是10天就完成了第一个版本,原则上就是降低入门成本,减少语法报错的可能性,不管结果死活了,能跑就是赢)
那么为啥Hoisting能提高编译效率呢?
在 JavaScript 执行的早期阶段,解析器会对整个代码进行作用域解析。将变量和函数的声明“提升”到作用域的顶部,可以使解析器在代码运行之前快速定位所有变量和函数,从而优化代码执行过程。如果没有提升,解析器必须动态检查代码块中是否存在变量,这会显著增加解析器的复杂性和性能开销。
那么为什么不把这逆天机制改了呢?
还是有请GPT:
1. 向下兼容性
JavaScript 已经被广泛应用,去掉提升会破坏大量现有代码的运行。因此,ECMAScript 保留了提升特性,同时通过引入let
和const
来改进。
2. 保留历史遗留行为
JavaScript 的早期设计中考虑了宽松的语法规则以适应快速开发环境,提升就是这些规则的遗留产物。删除提升会导致历史代码中的行为发生改变,难以维护。
3. 提升的替代方案更复杂
没有提升的语言通常需要更严格的声明规则(如 Python),这与 JavaScript 的宽松设计理念相悖。虽然提升容易被滥用,但它为开发提供了一种低门槛的动态方式。
行,我起码是信了,但我觉得历史原因是更多的。Git也是如此,即使现在SHA-1已经不安全了,但还是因为历史原因未用SHA-256去替换。
最佳实践
如何避免提升的陷阱?
- 使用
let
和const
它们也会被提升,但由于存在 暂时性死区(TDZ),未初始化前访问会抛出ReferenceError
,从而避免了意外行为。1
2console.log(x); // ReferenceError
let x = 10; - 严格模式
使用strict mode
,避免变量未声明时被使用:1
2
3;
console.log(a); // ReferenceError
a = 5; - 遵循“先声明,后使用”规则
避免依赖提升,让代码更加直观。
后记
呼,结束了,但感觉有开了个坑,作用域解析机制、**[[JS作用域]]** 或许有的一讲🫡,下次在说吧。
没想到这篇这么杂,有种风暴中心旁的混乱感,感觉还得在精进一波才能像庖丁解牛般讲Hoisting彻底讲清楚。