JS精进之Hoisting(提升)

前言

组会分享,初略的讲讲JS的Hoisting机制

Hoisting提升🏗️前置知识

啥是提升?

简单的说 Hoisting 是JS引擎在代码执行前将所有声明 提升到其所在作用域的顶部

这句话你可以在看完这篇文章后再回头看看这句话,真的是一言蔽之。

接下来我会简单讲解一下。

所有声明

JS中拢共有

  • 变常量var let const
  • 函数function
  • class
  • 模块import export
    是的这些都会提升,但是JS引擎在处理这些声明时会有不同的细节处理。

所在作用域

JS中拢共 global function block三个常规的作用域,这不会区别自己学去。

\[JS作用域\]

还有模块作用域 (Module Scope)、词法作用域 (Lexical Scope)、动态作用域 (运行时上下文)、私有作用域 (Private Scope),嘿🫠我也不会,下次学到的时候补上😁😁😁,初略的看了眼,除了module外和hoisting没多大关系(应该吧)

记住Hoisting提升 只会提升到其所在作用域的顶部,不会发生将function scope内的提升到global scope的事情。

详细讲解

var 的提升

var 声明的变量会被提升到作用域顶部,但仅提升声明部分,初始化(赋值)不会提升

提一句var会全局污染,即会绑定到window上,不了解的可以自己去查资料看var、let、const的区别.
例子:

javascript 复制代码
console.log(a); // undefined
var a = 5;
console.log(a); // 5

底层执行过程:

  1. JavaScript 引擎将 var a 提升到作用域顶部,等同于:

    javascript 复制代码
    var a;
    console.log(a); // undefined
    a = 5;
    console.log(a); // 5
  2. 因此,在变量赋值之前,a 的值是 undefined


[[let 和 const 的提升]]

letconst 也会被提升,但它们的变量在提升时会被放入一个称为 暂时性死区(Temporal Dead Zone, TDZ) 的区域,只有在声明之后才能访问。

例子:

javascript 复制代码
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;

底层执行过程:

  1. let b 被提升到作用域顶部,但在声明之前无法访问。
  2. 由于在 let b 之前尝试访问变量 b,引擎抛出 ReferenceError
啥是[[暂时性死区(Temporal Dead Zone, TDZ)]]

暂时性死区 是 JavaScript 中一种行为:

在作用域内,虽然变量已经"被提升"(即解析时已经被声明),但在实际声明和初始化完成之前,访问该变量会抛出 ReferenceError 错误。

这是为了避免变量在声明之前被意外访问或使用,增加代码的可预测性和安全性。

有死区和没死区粗暴的看就是,一个爆ReferenceError,一个爆undefined


函数的 Hoisting

函数又和前面的机制不同,函数声明的提升会将整个函数提升🫡🫡🫡,而函数表达式则只是会将变量提升,不提升函数。

1. 函数声明的提升

⭐函数声明会被完整地提升 到作用域顶部,因此在声明之前也可以调用函数。
例子:

javascript 复制代码
greet(); // Hello!
function greet() {
  console.log("Hello!");
}

底层执行过程:

  1. 函数声明 function greet() { ... }完整提升 到作用域顶部,等同于:

    javascript 复制代码
    function greet() {
      console.log("Hello!");
    }
    greet(); // Hello!

2. 函数表达式的提升

函数表达式(包括箭头函数)不会提升其赋值部分,仅变量名会被提升
例子:

javascript 复制代码
console.log(foo); // undefined
var foo = function () {
  console.log("Hello!");
};
foo(); // Hello!

底层执行过程:

  1. 只有 var foo 被提升,赋值部分不会提升,等同于:

    javascript 复制代码
    var foo;
    console.log(foo); // undefined
    foo = function () {
      console.log("Hello!");
    };
    foo(); // Hello!

使用 letconst 声明函数表达式时,会触发暂时性死区:

javascript 复制代码
console.log(bar); // ReferenceError
const bar = () => {
  console.log("Hi!");
};

class (类声明)

类声明也会被提升,但同样会存在暂时性死区,在声明之前无法访问。
示例

javascript 复制代码
const instance = new MyClass(); // ReferenceError报错,MyClass 在 TDZ 中
class MyClass {
  constructor(name) {
    this.name = name;
  }
}

import (模块声明)

import 总是在模块顶部执行,不能在代码块中使用。
示例

javascript 复制代码
import { myFunc } from './module.js';
myFunc();

export (模块声明)

特点

  • 用于导出变量、函数、类等,使其可以在其他模块中使用。
  • 支持命名导出(export {})和默认导出(export default)。
    示例
javascript 复制代码
export const myVar = 42;
export default function myFunc() {
  console.log("Hello!");
}

对比总结:

特性 var let const function class import/export
作用域 三作用域 三作用域 三作用域 函数作用域 块作用域 模块作用域
变量提升 是(值为 undefined 是(TDZ限制) 是(TDZ限制) 是(TDZ限制)
重复声明 可以 不可以 不适用 不适用 不适用
可重新赋值 不适用 不适用

[[Hoisting提升的先后顺序]]

看到这不知道你有没有疑惑,所有声明都会发生提升,那究竟谁比谁更能提升🫡🫡🫡,总不能左脚🦶踩右脚🦶升天不是。

在 JavaScript 中,不同类型的声明(如 varletconstfunctionclassimport/export)它们的执行优先级 和提升行为有所不同。以下是提升的先后顺序及具体规则:

提升顺序的原则
  1. 模块系统优先importexport 的静态绑定首先被处理。
  2. 函数声明优先:在代码执行前,函数声明会被提升到所在作用域的顶部。
  3. 变量声明依次提升var 早于 letconst,但值初始化按代码顺序进行。
  4. class 的提升特殊:类声明会被提升,但无法在声明前使用(存在 TDZ)。
提升优先级
类型 提升顺序 初始化可用性 是否受 TDZ 限制 特性解释
import 最高 在代码执行前绑定 模块加载的静态绑定,解析阶段完成,但无法访问
function 第二 在声明前可调用 函数声明整体提升,初始化为可调用函数
var 第三 在声明前可用(undefined 声明提升但初始化在原始位置
letconst 第四 声明前不可用 存在 TDZ,初始化需等到执行到声明语句才有效
class 最后 声明前不可用 类声明会提升,但使用前需显式定义
  • 最高优先级import/export 静态绑定最先解析。
  • 中间优先级function 声明高于 var
  • 最低优先级letconstclass,它们都受 TDZ 限制。
综合示例:优先级的对比

以下代码展示了多种声明的提升顺序和执行规则:

javascript 复制代码
console.log(func()); // "I am a function!"
console.log(x);      // undefined
// console.log(y);   // 报错:Cannot access 'y' before initialization
// console.log(z);   // 报错:Cannot access 'z' before initialization
// console.log(MyClass); // 报错:Cannot access 'MyClass' before initialization

function func() {
  return "I am a function!";
}

var x = 10;
let y = 20;
const z = 30;

class MyClass {
  constructor() {
    this.name = "Class Example";
  }
}

执行顺序

  1. 函数 func 提升并初始化。
  2. var x 声明提升但值为 undefined
  3. let yconst z 提升但进入 TDZ,未初始化。
  4. class MyClass 提升但进入 TDZ,未初始化。

为什么要提升?

这也是我当初了解到提升这一逆天特性时的最大疑惑,吃饱了撑着么要这么干?

我自己也解释不清,欧克,下面让我们看看GPT是怎么说的:

提升(Hoisting)是 JavaScript 的设计特性之一,它从一开始就融入了语言的实现中。设计提升的原因和历史背景与 JavaScript 的初衷、执行模型、开发环境的需求,以及语言设计的权衡密切相关。

  1. 简化解析和作用域解析,提升性能。
  2. 支持灵活的代码结构,适合快速开发和调试。
  3. 向下兼容,保留早期代码的行为一致性。
  4. 模仿 C 语言的声明模型,降低学习成本。
  5. 宽容开发者错误,降低入门门槛。

我们来挑重点看,主要就是俩点,一是能提高编译器效率 ,二是设计者就是想要易用🫡(毕竟是10天就完成了第一个版本,原则上就是降低入门成本,减少语法报错的可能性,不管结果死活了,能跑就是赢)

那么为啥Hoisting能提高编译效率呢?

在 JavaScript 执行的早期阶段,解析器会对整个代码进行作用域解析。将变量和函数的声明"提升"到作用域的顶部,可以使解析器在代码运行之前快速定位所有变量和函数,从而优化代码执行过程。如果没有提升,解析器必须动态检查代码块中是否存在变量,这会显著增加解析器的复杂性和性能开销。

那么为什么不把这逆天机制改了呢?

还是有请GPT:

1. 向下兼容性

JavaScript 已经被广泛应用,去掉提升会破坏大量现有代码的运行。因此,ECMAScript 保留了提升特性,同时通过引入 letconst 来改进。
2. 保留历史遗留行为

JavaScript 的早期设计中考虑了宽松的语法规则以适应快速开发环境,提升就是这些规则的遗留产物。删除提升会导致历史代码中的行为发生改变,难以维护。
3. 提升的替代方案更复杂

没有提升的语言通常需要更严格的声明规则(如 Python),这与 JavaScript 的宽松设计理念相悖。虽然提升容易被滥用,但它为开发提供了一种低门槛的动态方式。

行,我起码是信了,但我觉得历史原因是更多的。Git也是如此,即使现在SHA-1已经不安全了,但还是因为历史原因未用SHA-256去替换。

最佳实践

如何避免提升的陷阱?

  1. 使用 letconst
    它们也会被提升,但由于存在 暂时性死区(TDZ) ,未初始化前访问会抛出 ReferenceError,从而避免了意外行为。

    javascript 复制代码
    console.log(x); // ReferenceError
    let x = 10;
  2. 严格模式
    使用 strict mode,避免变量未声明时被使用:

    javascript 复制代码
    "use strict";
    console.log(a); // ReferenceError
    a = 5;
  3. 遵循"先声明,后使用"规则
    避免依赖提升,让代码更加直观。

后记

呼,结束了,但感觉有开了个坑,作用域解析机制[[JS作用域]] 或许有的一讲🫡,下次在说吧。

相关推荐
精彩极了吧11 小时前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合
好家伙VCC12 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
南极星100512 小时前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言
未来之窗软件服务12 小时前
未来之窗昭和仙君(六十五)Vue与跨地区多部门开发—东方仙盟练气
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·昭和仙君
baidu_2474386112 小时前
Android ViewModel定时任务
android·开发语言·javascript
嘿起屁儿整12 小时前
面试点(网络层面)
前端·网络
Dev7z12 小时前
基于 MATLAB 的铣削切削力建模与仿真
开发语言·matlab
不能隔夜的咖喱12 小时前
牛客网刷题(2)
java·开发语言·算法
VT.馒头12 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
小天源13 小时前
Error 1053 Error 1067 服务“启动后立即停止” Java / Python 程序无法后台运行 windows nssm注册器下载与报错处理
开发语言·windows·python·nssm·error 1053·error 1067