javascript2

js文档

https://javascript.info/devtools

use strict

ECMAScript 5 (ES5) 的出现。ES5 为该语言添加了新特性,并修改了一些现有特性。为了保持旧代码的兼容性,大多数此类修改默认情况下处于关闭状态。您需要使用特殊指令显式启用它们:”use strict”。

该指令看起来像一个字符串:”use strict”或’use strict’。当它位于脚本顶部时,整个脚本将以“现代”方式运行。

现代 JavaScript 支持“类”和“模块”——这些高级语言结构(我们稍后会详细介绍)会use strict自动启用。因此,如果我们使用它们,就不需要添加”use strict”指令。

所以,目前”use strict”;它只是脚本开头的一个有用组成部分。以后,当你的代码全部放在类和模块中时,就可以省略它了。

就目前而言,我们只能了解use strict一些基本情况。

在接下来的章节中,随着我们学习语言特性,我们将看到严格模式和旧模式之间的区别。幸运的是,区别并不多,而且它们实际上让我们的生活更便捷。

本教程中的所有示例均假定为严格模式,除非(极少数情况下)另有说明。

变量 & 常量

变量用let
常量用const

变量名应该具有清晰、明确的含义,描述它所存储的数据。

变量命名是编程中最重要也是最复杂的技能之一。只需看一眼变量名,就能分辨出代码是出自新手之手还是经验丰富的开发者之手。

在实际项目中,大部分时间都花在修改和扩展现有代码库上,而不是从头开始编写全新的代码。当我们在忙其他事情一段时间后再回到之前的代码时,如果变量名称清晰明了,查找信息就容易得多。

请在声明变量之前花时间仔细考虑其名称的恰当性。这样做会让你受益匪浅。

一些值得遵循的规则包括:

使用易于理解的名称,例如userName或shoppingCart。
除非你清楚自己在做什么,否则请避免使用缩写或简称,例如a、b和c。
变量名应尽可能简洁明了,并尽可能具有描述性。例如,adata和 b就是糟糕的变量value名,它们没有任何意义。只有当代码上下文能够非常清晰地表明变量引用的是哪个数据或值时,才可以使用这类变量名。
在团队内部以及你们自己心中达成共识。如果网站访问者被称为“用户”,那么我们应该用其他名称来命名相关的变量currentUser,而newUser不是用“用户”或currentVisitor“访客newManInTown”。

数据类型

js有8种数据类型

  1. 数字

  2. 大数字

  3. 字符串

  4. 布尔值

  5. null值
    在 JavaScript 中,null它不像其他一些语言那样是“指向不存在对象的引用”或“空指针”。
    它只是一个特殊值,表示“无”、“空”或“值未知”。
    上面的代码表明该值age未知。

  6. 未定义undefined
    其含义undefined是“未赋值”。

如果一个变量已声明但未赋值,则其值为undefined

  1. object
  2. typeof运算符
    该typeof运算符返回操作数的类型。当我们想要区别处理不同类型的值,或者只是想进行快速检查时,它非常有用。

调用该函数会typeof x返回一个包含类型名称的字符串

JavaScript 中有 8 种基本数据类型。

七种基本数据类型:
number对于任何类型的数字:整数或浮点数,整数受限制。±(253-1)
bigint对于任意长度的整数。
string对于字符串而言,一个字符串可以包含零个或多个字符,没有单独的单字符类型。
boolean为了true/ false。
null对于未知值——具有单个值的独立类型null。
undefined对于未赋值——具有单个值的独立类型undefined。
symbol用于唯一标识符。

还有一种非原始数据类型:
object适用于更复杂的数据结构。
该typeof运算符允许我们查看变量中存储的是哪种类型。

通常用作typeof x,但typeof(x)也可以使用。
返回一个包含类型名称的字符串,例如”string”。
对于null返回值”object”——这是语言上的错误,它实际上并不是一个对象。

switch 和if的区别是什么,在js中

在 JavaScript 中,switchif 都是条件判断语句,核心目的是根据不同条件执行不同代码,但两者的判断逻辑、使用场景、执行机制有显著区别,选择时需结合具体需求。以下是详细对比:

一、核心区别总览

对比维度 if 语句 switch 语句
判断逻辑 基于「布尔条件表达式」(true/false)判断 基于「表达式的值」与 case 进行「全等匹配(===)」
适用场景 复杂条件(范围、多表达式组合)、少量分支 单值匹配(固定选项)、多分支(可读性更优)
执行流程 顺序判断,满足一个条件后直接跳出(除非无 else if 匹配 case 后执行,需 break 终止(否则穿透)
灵活性 极高(支持任意布尔逻辑:&&/`
可读性 分支少/条件复杂时清晰,分支多时冗余 分支多时更简洁(结构化展示选项)
性能 分支越多,判断效率越低(逐个校验) 分支较多时可能更优(引擎可能优化为查找表)

二、关键差异详解(附代码示例)

1. 判断逻辑:「布尔条件」vs「值全等匹配」

if 的核心是判断条件是否为 true,条件可以是任意布尔表达式(范围、多条件组合等);
switch 的核心是将「表达式结果」与「case 值」做全等比较(===),必须类型和值都一致才匹配。

示例1:范围判断(只能用 if

需求:根据成绩分级(90+优秀、80+良好、70+及格、否则不及格)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const score = 85;

// if 可以直接写范围条件
if (score >= 90) {
console.log("优秀");
} else if (score >= 80) {
console.log("良好"); // 执行
} else if (score >= 70) {
console.log("及格");
} else {
console.log("不及格");
}

// switch 无法直接写范围,只能强行枚举值(极冗余,不推荐)
switch (true) { // 表达式为 true,case 需是布尔值
case score >= 90:
console.log("优秀");
break;
case score >= 80:
console.log("良好");
break;
// ... 省略其他 case
}
示例2:固定值匹配(switch 更简洁)

需求:根据状态码提示信息(200=成功、404=未找到、500=服务器错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const statusCode = 404;

// if 写法(分支多了会冗余)
if (statusCode === 200) {
console.log("请求成功");
} else if (statusCode === 404) {
console.log("资源未找到"); // 执行
} else if (statusCode === 500) {
console.log("服务器错误");
} else {
console.log("未知错误");
}

// switch 写法(结构化展示,更清晰)
switch (statusCode) {
case 200:
console.log("请求成功");
break;
case 404:
console.log("资源未找到"); // 执行
break;
case 500:
console.log("服务器错误");
break;
default: // 无匹配时执行(类似 if 的 else)
console.log("未知错误");
}

2. 执行流程:「顺序跳出」vs「穿透特性」

  • if 语句:一旦某个 if/else if 条件成立,执行对应代码后直接跳出整个 if 结构(无需额外操作);
  • switch 语句:匹配到 case 后,会继续执行后续所有 case(直到遇到 breakreturn),这就是「穿透特性」(可利用但需谨慎)。
示例3:switch 的穿透特性(双刃剑)

需求:12/1/2 月是冬季,3/4/5 月是春季(利用穿透减少重复代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const month = 2;

switch (month) {
case 12:
case 1:
case 2: // 匹配 12、1、2 时都会执行下面的代码(穿透)
console.log("冬季");
break; // 终止穿透
case 3:
case 4:
case 5:
console.log("春季");
break;
// ... 其他季节
}

⚠️ 注意:如果忘记写 break,会导致代码“穿透”到下一个 case 执行(例如上面去掉 case 2 后的 break,会继续执行 case 3 的逻辑),这可能是 bug 来源,也可能是刻意优化。

3. 灵活性与使用场景

  • if 的场景:

    • 条件是「范围」(如 age > 18score >= 60 && score < 80);
    • 条件是「多表达式组合」(如 user.type === 'admin' && user.status === 'active');
    • 分支数量少(1-3 个),条件复杂。
  • switch 的场景:

    • 条件是「单值匹配」(如状态码、类型枚举、下拉框选项);
    • 分支数量多(4 个以上),需要结构化展示(比一堆 else if 更易读);
    • 需利用「穿透特性」减少重复代码(如多个值对应同一逻辑)。

三、补充细节

  1. switch 的「全等匹配」:
    case 与表达式的匹配是 ===(严格相等),类型不同会匹配失败。例如:

    1
    2
    3
    4
    5
    const x = "1";
    switch (x) {
    case 1: console.log("数字1"); break; // 不匹配(字符串 vs 数字)
    case "1": console.log("字符串1"); break; // 匹配
    }
  2. default 的位置:
    default 是「默认分支」(无匹配时执行),可以放在任意位置(不一定在最后),但建议放在末尾,且需加 break(否则会穿透)。

  3. switch 的表达式支持任意类型:
    表达式可以是数字、字符串、布尔值、甚至函数调用(只要返回一个值),例如:

    1
    2
    3
    4
    5
    switch (getUserType()) { // getUserType() 返回 'admin'/'user'/'guest'
    case 'admin': // ...
    case 'user': // ...
    // ...
    }

四、总结

选择依据 推荐用
范围判断/多条件组合 if
单值匹配/多分支 switch
分支少(1-3个) if
分支多(4个以上) switch
需利用穿透特性 switch

简单记:复杂条件用 if,固定选项用 switch,核心是让代码更易读、更易维护。

异步编程(async/await)是什么?在js中

在 JavaScript 中,**async/await 是基于 Promise 的「语法糖」**,核心目的是让异步代码的写法更简洁、更接近同步代码的逻辑,同时解决了 Promise 链(.then() 嵌套)可能出现的冗余问题,让异步流程更易读、易调试。

要理解 async/await,首先要明确一个前提:它没有改变 JS 异步编程的本质(依然是基于事件循环、非阻塞),只是简化了语法,底层仍然依赖 Promise 实现。

一、先搞懂:为什么需要 async/await

async/await 出现前,JS 异步编程主要靠「回调函数」和「Promise」:

  • 回调函数:容易出现「回调地狱」(嵌套层级过深,代码可读性差);
  • Promise:通过 .then() 链式调用解决了回调地狱,但多个异步操作串行时,.then() 链依然会显得冗长,逻辑不够直观。

async/await 正是为了解决这个问题——让异步代码像同步代码一样「线性书写」,逻辑更清晰。

示例对比:Promise vs async/await

需求:先请求用户信息,再根据用户 ID 请求用户订单(两个异步操作串行)

1. Promise 写法(链式调用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 模拟异步请求:获取用户信息
function getUser() {
return new Promise((resolve) => {
setTimeout(() => resolve({ id: 1, name: "张三" }), 1000);
});
}

// 模拟异步请求:根据用户ID获取订单
function getOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([{ id: 101, goods: "手机" }]), 1000);
});
}

// Promise 链式调用
getUser()
.then(user => {
console.log("用户信息:", user);
return getOrders(user.id); // 传递用户ID给下一个异步操作
})
.then(orders => {
console.log("用户订单:", orders);
})
.catch(error => {
console.log("出错了:", error); // 捕获所有异步错误
});
2. async/await 写法(同步风格)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 同样的两个异步函数(返回Promise)
function getUser() { /* ... 同上 ... */ }
function getOrders(userId) { /* ... 同上 ... */ }

// 用 async 修饰函数,内部可使用 await
async function fetchData() {
try {
const user = await getUser(); // 暂停执行,等待Promise成功 resolve
console.log("用户信息:", user);

const orders = await getOrders(user.id); // 基于上一个异步结果,继续等待
console.log("用户订单:", orders);
} catch (error) {
console.log("出错了:", error); // 捕获所有 await 抛出的错误
}
}

fetchData(); // 调用异步函数

可以明显看到:async/await 去掉了 .then() 链,代码逻辑和同步代码完全一致,可读性大幅提升。

二、核心语法:asyncawait 各自的作用

async/await 是成对出现的(await 必须在 async 函数内使用),两者分工明确:

1. async 关键字

  • 作用:修饰一个函数,使其成为「异步函数」;
  • 返回值:异步函数的返回值会被自动包装成一个 Promise
    • 如果函数内返回一个普通值(如数字、对象),则返回 Promise.resolve(普通值)
    • 如果函数内抛出错误(如 throw new Error()),则返回 Promise.reject(错误)
    • 如果函数内直接返回一个 Promise,则直接返回该 Promise(不额外包装)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 返回普通值 → 包装成 resolved Promise
async function fn1() {
return "hello";
}
fn1().then(res => console.log(res)); // 输出:hello

// 2. 抛出错误 → 包装成 rejected Promise
async function fn2() {
throw new Error("出错了");
}
fn2().catch(err => console.log(err.message)); // 输出:出错了

// 3. 返回 Promise → 直接返回该 Promise
async function fn3() {
return Promise.resolve("直接返回Promise");
}
fn3().then(res => console.log(res)); // 输出:直接返回Promise

2. await 关键字

  • 作用:暂停 async 函数的执行,等待后面的「Promise 决议」(成功或失败);
  • 只能用在 async 函数内(普通函数中使用会报错);
  • 等待结果:
    • 如果 await 后面是一个 Promise:会等待该 Promise 成功(resolve),并返回 resolve 的值;如果 Promise 失败(reject),则会抛出异常,需要用 try/catch 捕获;
    • 如果 await 后面是一个非 Promise 值(如数字、字符串):会直接返回该值(相当于 await Promise.resolve(非Promise值))。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function testAwait() {
// 等待 Promise 成功
const res1 = await Promise.resolve("成功的值");
console.log(res1); // 输出:成功的值

// 等待非 Promise 值(自动包装成 resolved Promise)
const res2 = await 123;
console.log(res2); // 输出:123

// 等待 Promise 失败(会抛出异常,需用 try/catch 捕获)
try {
const res3 = await Promise.reject("失败的原因");
} catch (err) {
console.log(err); // 输出:失败的原因
}
}

testAwait();

三、关键特性:错误处理

由于 await 会把 Promise 的 reject 转化为「异常」,因此 async/await 必须配合 try/catch 处理错误,否则未捕获的异常会导致程序崩溃(或被全局错误监听捕获)。

两种错误处理场景:

  1. 多个异步操作共享错误处理(一个出错就终止):

    1
    2
    3
    4
    5
    6
    7
    8
    async function fetchData() {
    try {
    const user = await getUser(); // 若失败,直接进入 catch
    const orders = await getOrders(user.id); // 上一步失败则不会执行
    } catch (error) {
    console.log("任意一个异步操作出错:", error);
    }
    }
  2. 多个异步操作独立错误处理(一个出错不影响其他):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    async function fetchData() {
    // 每个 await 单独 try/catch
    try {
    const user = await getUser();
    } catch (err) {
    console.log("获取用户失败:", err);
    }

    try {
    const orders = await getOrders(1); // 假设ID固定,即使上一步失败也执行
    } catch (err) {
    console.log("获取订单失败:", err);
    }
    }

四、常见使用场景

1. 串行异步操作(按顺序执行)

多个异步操作依赖前一个的结果(如先登录获取 token,再用 token 请求数据),async/await.then() 更直观:

1
2
3
4
5
6
7
8
9
10
async function loginAndFetch() {
try {
const token = await login("用户名", "密码"); // 第一步:登录获取token
const userInfo = await fetchUserInfo(token); // 第二步:用token获取用户信息
const data = await fetchDataByUser(userInfo.id); // 第三步:用用户ID获取数据
return data;
} catch (err) {
console.log("流程出错:", err);
}
}

2. 并行异步操作(同时执行,提高效率)

多个异步操作互不依赖,需要同时执行(如同时请求商品列表和分类列表),可配合 Promise.all() 使用(await 等待所有 Promise 完成):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function fetchParallel() {
try {
// 同时启动两个异步请求(并行执行,总耗时取最长的那个)
const promise1 = fetchGoods(); // 请求商品列表
const promise2 = fetchCategories(); // 请求分类列表

// 等待两个请求都完成,返回结果数组
const [goods, categories] = await Promise.all([promise1, promise2]);
console.log("商品列表:", goods);
console.log("分类列表:", categories);
} catch (err) {
console.log("任意一个请求失败:", err); // 只要有一个失败,就进入catch
}
}

3. 可选异步操作(失败不影响主流程)

如果某个异步操作失败后,主流程仍需继续,可使用 Promise.catch() 单独处理该操作的错误:

1
2
3
4
5
6
7
8
async function fetchWithFallback() {
// 即使 fetchRecommend 失败,也不会影响主流程
const recommend = await fetchRecommend().catch(() => "默认推荐内容");
const goods = await fetchGoods(); // 正常执行

console.log("推荐内容:", recommend);
console.log("商品列表:", goods);
}

五、注意事项(避坑点)

  1. await 不能在普通函数中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 错误:普通函数内不能用 await
    function fn() {
    const res = await fetchData(); // 语法错误
    }

    // 正确:必须用 async 修饰函数
    async function fn() {
    const res = await fetchData();
    }
  2. async 函数不会阻塞主线程
    await 只会暂停当前 async 函数的执行,不会阻塞整个 JS 主线程(其他同步代码或异步任务仍会正常执行):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function fn() {
    console.log("开始");
    await new Promise(resolve => setTimeout(resolve, 1000)); // 暂停1秒
    console.log("async函数内继续执行");
    }

    fn();
    console.log("主线程同步代码(不会被阻塞)");

    // 输出顺序:
    // 开始 → 主线程同步代码(不会被阻塞) → (1秒后)async函数内继续执行
  3. Promise.all() 的「快速失败」特性
    如果 Promise.all() 中的任意一个 Promise 失败,整个 Promise.all() 会立即 reject,其他未完成的 Promise 仍会继续执行,但结果会被忽略。如果需要「所有 Promise 都执行完,不管成功失败」,可使用 Promise.allSettled()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    async function fetchAllSettled() {
    const promise1 = fetchGoods();
    const promise2 = fetchInvalidUrl(); // 故意请求错误地址

    // 等待所有 Promise 完成(无论成功/失败)
    const results = await Promise.allSettled([promise1, promise2]);

    // 遍历结果,分别处理成功和失败
    results.forEach((result, index) => {
    if (result.status === "fulfilled") {
    console.log(`第${index+1}个请求成功:`, result.value);
    } else {
    console.log(`第${index+1}个请求失败:`, result.reason);
    }
    });
    }

六、总结

  1. 本质async/await 是 Promise 的语法糖,不改变 JS 异步本质;
  2. 核心优势:让异步代码像同步代码一样线性书写,可读性、可维护性远超回调和 Promise 链;
  3. 使用规则
    • async 修饰函数,返回 Promise;
    • await 只能在 async 函数内使用,等待 Promise 决议;
    • 必须用 try/catch 处理 await 可能抛出的错误;
  4. 适用场景
    • 串行异步操作(依赖前一个结果);
    • 并行异步操作(配合 Promise.all());
    • 复杂异步流程(多分支、条件判断)。

如今,async/await 已成为 JS 异步编程的「最佳实践」,几乎完全替代了复杂的 .then() 链,是前端开发中必须掌握的核心语法。

回调地狱是什么?

在 JavaScript 中,回调地狱(Callback Hell) 是指「异步操作依赖前一个异步操作结果」时,使用回调函数嵌套导致的「代码层级过深、可读性差、维护困难」的现象。

它的核心原因是:JS 异步操作(如定时器、网络请求、文件读写)默认通过「回调函数」通知结果,当多个异步操作需要按顺序串行执行(后一个操作依赖前一个的结果)时,只能把后一个回调嵌套在前一个回调的内部,最终形成「金字塔式」的嵌套结构。

一、直观感受:回调地狱长什么样?

举个实际场景:
需求是「先获取用户信息 → 再根据用户 ID 获取订单 → 最后根据订单 ID 获取物流信息」,三个异步操作必须按顺序执行(后一个依赖前一个的结果)。

回调地狱示例(嵌套层级深)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 模拟异步请求:获取用户信息(回调函数接收结果)
function getUser(callback) {
setTimeout(() => {
const user = { id: 1, name: "张三" };
callback(null, user); // 异步完成后调用回调,传递结果
}, 1000);
}

// 模拟异步请求:根据用户ID获取订单
function getOrders(userId, callback) {
setTimeout(() => {
const orders = [{ id: 101, goods: "手机", userId: 1 }];
callback(null, orders[0]); // 取第一个订单
}, 1000);
}

// 模拟异步请求:根据订单ID获取物流
function getLogistics(orderId, callback) {
setTimeout(() => {
const logistics = { orderId: 101, status: "已发货" };
callback(null, logistics);
}, 1000);
}

// 回调嵌套实现串行逻辑(回调地狱出现!)
getUser((err, user) => {
if (err) return console.log("获取用户失败:", err);

// 第一个回调内部嵌套第二个异步操作
getOrders(user.id, (err, order) => {
if (err) return console.log("获取订单失败:", err);

// 第二个回调内部嵌套第三个异步操作
getLogistics(order.id, (err, logistics) => {
if (err) return console.log("获取物流失败:", err);

// 三层嵌套后,才拿到最终结果
console.log("用户:", user.name);
console.log("订单:", order.goods);
console.log("物流:", logistics.status);
});
});
});

这段代码的问题很明显:

  • 嵌套层级越来越深(上面是 3 层,实际场景可能 5 层以上),形成「右缩进金字塔」;
  • 代码逻辑被切割在不同的回调函数内,阅读时需要「跳着看」,理解成本高;
  • 维护困难:如果要在中间加一个步骤(比如获取用户后校验权限),需要修改多层嵌套结构;
  • 错误处理分散:每个回调都要单独处理 err,冗余且容易遗漏。

二、回调地狱的核心危害

  1. 可读性极差:嵌套层级越深,代码越像「金字塔」,后续开发者需要逐层梳理逻辑关系;
  2. 维护成本高:修改某一层逻辑时,可能需要改动外层或内层的回调,容易引发 bug;
  3. 错误处理繁琐:每个异步操作的错误都要单独捕获(如上面的 if (err)),无法统一处理;
  4. 调试困难:报错时的调用栈会被回调函数割裂,难以定位是哪一层异步操作出了问题。

三、怎么解决回调地狱?

回调地狱的本质是「回调嵌套导致的流程碎片化」,解决思路是「让异步流程扁平化」,JS 中主要有两种主流方案:

方案 1:使用 Promise 链式调用(替代嵌套回调)

Promise 是为了解决回调地狱而生的异步规范,它通过 .then() 方法将异步操作串联起来,避免了嵌套。

改造上面的示例(先把回调函数改成返回 Promise):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1. 把回调函数改成返回 Promise
function getUser() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve({ id: 1, name: "张三" }), 1000);
});
}

function getOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve({ id: 101, goods: "手机", userId: 1 }), 1000);
});
}

function getLogistics(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve({ orderId: 101, status: "已发货" }), 1000);
});
}

// 2. Promise 链式调用(扁平化流程)
getUser()
.then(user => {
console.log("用户:", user.name);
return getOrders(user.id); // 返回 Promise,衔接下一个 .then()
})
.then(order => {
console.log("订单:", order.goods);
return getLogistics(order.id); // 继续衔接
})
.then(logistics => {
console.log("物流:", logistics.status);
})
.catch(err => {
console.log("出错了:", err); // 统一捕获所有异步错误(无需单独处理)
});

优势:

  • 嵌套层级消失,代码横向展开,逻辑线性化;
  • 错误处理统一:所有 .then() 中的错误都会被末尾的 .catch() 捕获;
  • 可读性比回调嵌套提升明显。

方案 2:使用 async/await(最简洁的方案)

async/await 是 Promise 的语法糖,能让异步代码完全像同步代码一样书写,是目前解决回调地狱的「最佳实践」。

改造上面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 复用上面返回 Promise 的三个函数
function getUser() { /* ... */ }
function getOrders(userId) { /* ... */ }
function getLogistics(orderId) { /* ... */ }

// async 函数内使用 await 等待异步结果
async function fetchData() {
try {
const user = await getUser(); // 等待用户信息(暂停,不阻塞主线程)
console.log("用户:", user.name);

const order = await getOrders(user.id); // 等待订单(依赖用户ID)
console.log("订单:", order.goods);

const logistics = await getLogistics(order.id); // 等待物流(依赖订单ID)
console.log("物流:", logistics.status);
} catch (err) {
console.log("出错了:", err); // 统一捕获所有错误
}
}

fetchData();

优势:

  • 代码完全线性化,和同步逻辑几乎一致,可读性最强;
  • 错误处理更直观(try/catch 包裹所有异步操作);
  • 调试方便:断点可以像同步代码一样逐行执行,无需跳回调。

四、补充:避免回调地狱的其他小技巧

  1. 拆分函数:把嵌套的回调函数拆成独立的命名函数,即使使用回调,也能减少嵌套层级:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 拆分独立函数(避免嵌套)
    function handleUser(err, user) {
    if (err) return console.log(err);
    getOrders(user.id, handleOrder); // 调用下一个独立函数
    }

    function handleOrder(err, order) {
    if (err) return console.log(err);
    getLogistics(order.id, handleLogistics);
    }

    function handleLogistics(err, logistics) {
    if (err) return console.log(err);
    console.log(logistics);
    }

    // 初始调用(无嵌套)
    getUser(handleUser);
  2. 使用工具库:早期还有 async.js 这类库专门处理异步流程,但现在 Promise + async/await 已原生支持,基本无需依赖第三方库。

五、总结

  • 回调地狱是什么:异步回调嵌套导致的代码层级过深、可读性差、维护困难的现象;
  • 为什么会出现:多个串行异步操作依赖前一个结果,只能通过回调嵌套实现;
  • 怎么解决:核心是「流程扁平化」,优先使用 async/await(最简洁),其次是 Promise 链式调用;
  • 核心结论:回调地狱本身不是语法错误,而是代码设计问题,现代 JS 已通过 Promise 和 async/await 完美解决,实际开发中应尽量避免直接使用嵌套回调。

是所有语言都存在回调地狱的问题,还是只有javascript存在?

你的两个疑问其实是“Solidity开发是否必须精通JS”和“回调地狱的语言边界”,这两个点都需要结合实际开发场景拆解,结论先明确:Solidity开发不需要“精通”JS(有替代方案),回调地狱也不是JS独有——它是“异步回调嵌套”的通用问题,现在多数语言都有成熟解决方案。下面详细展开:

一、先澄清:Solidity开发≠必须精通JavaScript

你之所以有“必须精通JS”的错觉,是因为入门时接触的Truffle、Hardhat等工具默认用JS/TS写测试脚本,但这只是“主流方案”,不是“唯一方案”:

1. JS/TS的真实定位:“测试/工具链的常用语言”,而非“核心开发语言”

Solidity开发的核心是写合约(用Solidity),JS/TS的作用主要是:

  • 写测试脚本(验证合约功能/安全);
  • 写部署脚本(把合约部署到链上);
  • 写前端交互逻辑(比如DApp调用合约)。

但这些工作对JS的要求是“熟练使用”(比如掌握异步、Promise、测试库API),而非“精通”(比如深入JS引擎、框架源码)——大部分场景下,掌握基础语法+测试工具(Hardhat/Waffle)的使用即可。

2. 完全不用JS的替代方案:Foundry

现在行业内(尤其是大厂、安全团队)越来越流行用 Foundry 开发测试,它的核心特点是:

  • 用Solidity写测试脚本(不用JS/TS),直接用Solidity语法模拟合约交互、断言结果;
  • 更贴近合约逻辑,测试效率更高(比如模拟攻击场景更直观);
  • 适合深度安全测试(比如模糊测试、形式化验证)。

比如用Foundry测试“管理员才能暂停合约”,直接用Solidity写测试用例,完全不涉及JS:

1
2
3
4
5
6
7
8
9
10
11
12
// Foundry测试示例(Solidity语法)
function testOnlyAdminCanPause() public {
// 普通用户调用暂停函数,预期失败
vm.prank(user); // 模拟普通用户身份
vm.expectRevert("Only admin"); // 预期 revert 信息
myContract.pause();

// 管理员调用暂停函数,预期成功
vm.prank(admin); // 模拟管理员身份
myContract.pause();
assertEq(myContract.paused(), true); // 断言结果
}

所以结论:Solidity开发的核心是Solidity本身,JS/TS是“工具层语言”,可替代,无需精通——入门时可以先熟练JS+Hardhat,后续如果想侧重安全测试,再学Foundry即可。

二、回调地狱:不是JS独有,是“异步回调嵌套”的通用问题

要搞懂这个问题,先明确“回调地狱”的本质:

1. 回调地狱的定义

当需要连续执行多个异步操作(比如“先部署合约→再给合约转账→再调用合约函数”),且后一个操作依赖前一个的结果时,如果用“回调函数嵌套”的方式实现,就会出现代码层层缩进、可读性差、维护难的情况——这就是回调地狱。

它的核心是“异步+回调嵌套”,和语言无关,只要语言支持“异步回调”,就可能出现这个问题。

2. 哪些语言会有回调地狱?(分两类)

(1)有回调地狱风险的语言(支持异步回调,早期无更好方案)
  • JavaScript/TypeScript:最典型的是早期Node.js(比如用fs.readFile嵌套读取多个文件),但现在已经用Promise+async/await彻底解决了;
  • Python:早期用requests+回调函数处理异步(比如爬虫异步请求),会出现嵌套问题,现在有asyncio+await
  • Java:早期用Runnable+Handler处理异步(比如Android开发),嵌套回调会导致“回调地狱”,现在用CompletableFuture+lambda解决;
  • **C#**:早期用委托(Delegate)做异步回调,会有嵌套问题,现在用Task+async/await

举个JS早期的回调地狱例子(现在没人这么写了):

1
2
3
4
5
6
7
8
9
10
11
// 早期Node.js读取文件的回调地狱
fs.readFile('file1.txt', (err1, data1) => {
if (err1) throw err1;
fs.readFile('file2.txt', (err2, data2) => {
if (err2) throw err2;
fs.readFile('file3.txt', (err3, data3) => {
if (err3) throw err3;
console.log(data1 + data2 + data3); // 三层嵌套,可读性差
});
});
});
(2)基本没有回调地狱的语言(两种情况)
  • 本身是同步执行的语言:比如Solidity、Java(非异步场景)、Go(同步代码)——因为没有“异步操作”,自然不需要回调,也就不会有回调地狱;
  • 有原生异步方案(非回调嵌套)的语言
    • Go:用goroutine+channel处理异步,无需回调嵌套;
    • Rust:用Future+async/await,异步逻辑线性编写,无嵌套;
    • Solidity:合约执行是完全同步的(链上操作都是阻塞式的,比如转账、调用函数会立即返回结果),不存在“异步回调”,所以绝对不会有回调地狱;
    • PHP:早期以同步为主,现在支持async/await,但很少用回调嵌套。

3. 关键结论:回调地狱是“历史问题”,现在已被彻底解决

不管是JS还是其他语言,现在都不会再用“回调嵌套”的方式写异步代码,而是用更优雅的方案:

  • JS/TS:Promise+async/await(把嵌套改成线性代码);
  • Python:asyncio+await
  • Java:CompletableFuture+thenApply(链式调用);
  • C#:Task+async/await

比如上面JS的回调地狱例子,现在用async/await改写后,完全没有嵌套:

1
2
3
4
5
6
7
// 现在的JS写法(无回调地狱)
async function readFiles() {
const data1 = await fs.promises.readFile('file1.txt');
const data2 = await fs.promises.readFile('file2.txt');
const data3 = await fs.promises.readFile('file3.txt');
console.log(data1 + data2 + data3); // 线性代码,可读性强
}

而你在Solidity测试中用JS/TS时,也都是用async/await写测试脚本(Hardhat默认支持),比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Hardhat测试脚本(async/await,无回调地狱)
describe("MyContract", function () {
it("should pause only by admin", async function () {
const [admin, user] = await ethers.getSigners(); // 异步获取账号
const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy(admin.address); // 异步部署

// 普通用户调用暂停,预期失败
await expect(myContract.connect(user).pause()).to.be.revertedWith("Only admin");
// 管理员调用暂停,预期成功
await myContract.connect(admin).pause();
expect(await myContract.paused()).to.be.true;
});
});

所以你完全不用为“回调地狱”担心——现在的JS开发已经没有这个问题了。

三、对你的学习启示

  1. 不用强迫自己“精通JS”:Solidity开发的核心是合约编写和安全测试,JS只要掌握“基础语法+异步(Promise/async/await)+Hardhat测试库使用”就够了,后续可以学Foundry用Solidity写测试,彻底摆脱JS依赖;
  2. 不用害怕“回调地狱”:它是早期异步编程的历史问题,现在所有主流语言都有解决方案,实际开发中几乎遇不到;
  3. JS的核心价值(如果要学):除了测试,JS还能帮你写DApp前端(比如用React+Ethers.js调用合约),这是Solidity开发的加分项,但不是必选项。

总结:Solidity开发≠精通JS,回调地狱也不是JS独有(且已被解决)。你可以先聚焦Solidity本身和“JS基础+Hardhat测试”,后续根据需求选择是否深入JS(比如做前端)或转向Foundry(比如侧重安全)。

同步执行的语言和异步执行的语言有什么区别

同步执行语言和异步执行语言的核心区别,在于 代码执行的“顺序规则”和“资源利用方式” ——本质是“是否允许程序在等待某个操作完成时,去做其他事情”。结合你熟悉的 Solidity(同步)、JavaScript(异步),以及传统开发经验,用通俗的逻辑+实际场景拆解,你会更容易理解:

一、先搞懂核心定义:用“排队办事”比喻

1. 同步执行:“排队等,做完一个再做下一个”

就像去银行办事,所有任务按顺序排队,必须等前一个任务完全完成,后一个才开始执行
如果前一个任务卡住(比如等待文件读取、网络请求),整个程序会“阻塞”(暂停),直到这个任务结束,后续代码才会继续跑。

2. 异步执行:“多窗口并行,不用死等”

就像餐厅同时接待多个顾客,一个任务如果需要等待(比如煮菜要5分钟),程序不会闲着,而是去处理其他不需要等待的任务;等之前的任务完成后(比如菜煮好了),再回头处理它的后续逻辑。
核心是“非阻塞”——等待过程中不浪费时间,提高资源利用率。

二、关键区别对比(表格更清晰)

对比维度 同步执行语言 异步执行语言
执行顺序 严格按代码书写顺序,串行执行 不按固定顺序,“谁先完成谁先执行”,并行(逻辑上)
等待操作的处理方式 阻塞:等待时程序暂停,啥也不做 非阻塞:等待时去执行其他任务,不浪费资源
核心场景 不需要等待的操作(CPU密集型):比如计算、逻辑判断、合约执行 需要等待的操作(IO密集型):比如网络请求、文件读取、链上交互
代码复杂度 逻辑简单,容易调试(顺序明确) 逻辑稍复杂(需处理“回调/异步结果”),但现在有 async/await 简化
资源利用率 较低(等待时CPU闲置) 较高(等待时CPU处理其他任务)
典型语言/场景 Solidity、Java(同步代码)、C(基础场景):比如合约转账、数学计算 JavaScript/TypeScript、Python(asyncio)、Go(goroutine):比如DApp前端请求、合约测试(部署/调用链上合约)

三、用你熟悉的场景,拆解具体差异

结合你学过的 Solidity 开发,举2个实际例子,瞬间懂:

例子1:Solidity 合约执行(同步)

Solidity 是 纯同步语言,链上合约的所有操作都必须同步执行——因为区块链需要保证“交易结果的确定性”(比如转账后余额必须立即更新,不能出现“异步延迟导致的余额混乱”)。
比如一个合约函数:

1
2
3
4
5
6
function transfer(address to, uint256 amount) public {
require(balance[msg.sender] >= amount, "余额不足"); // 任务1:检查余额
balance[msg.sender] -= amount; // 任务2:扣钱
balance[to] += amount; // 任务3:加钱
emit Transfer(msg.sender, to, amount); // 任务4:发事件
}

执行时必须按“1→2→3→4”的顺序,任务1没完成,任务2绝对不会开始;如果任务1失败(余额不足),后续所有任务都直接终止——这就是同步的“确定性”,也是区块链安全的基础(不能让转账过程中出现“半成功半失败”)。

例子2:JS 写 Solidity 测试(异步)

你用 Hardhat/JS 写测试时,经常会写 await,这就是典型的异步场景:

1
2
3
4
5
async function testTransfer() {
const contract = await ethers.getContractFactory("MyToken"); // 任务1:加载合约(需读文件+网络交互,要等)
const deployedContract = await contract.deploy(); // 任务2:部署合约(链上操作,要等)
const balance = await deployedContract.balanceOf(admin.address); // 任务3:查余额(链上查询,要等)
}
  • 如果是同步执行:任务1没完成(合约没加载好),就不能开始任务2,且等待时 JS 引擎完全闲置;
  • 实际是异步执行:await 会让当前任务“暂停等待”,但 JS 引擎可以去处理其他测试用例的初始化、链上连接等操作,等当前任务完成后再继续——这样能大幅提高测试效率(不用在每个链上操作的等待时间里“空转”)。

四、关键澄清:3个容易踩的误区

1. 异步 ≠ 多线程(很多人混淆)

  • 多线程:真正的“同时执行多个任务”(比如电脑同时开浏览器、微信、编辑器),需要CPU多核支持;
  • 异步:逻辑上的“并行”,但可能是单线程实现的(比如JS是单线程,靠“事件循环”处理异步任务)——比如JS引擎同一时间只能执行一个代码,但通过“等待时切换任务”,让人感觉是“同时在做多个事”。

2. 没有“绝对更好”的语言,只有“适配场景”

  • 同步适合:需要“顺序确定性”“无依赖的快速操作”——比如合约执行(必须保证每一步的结果正确后再下一步)、数学计算(CPU密集型,不需要等待);
  • 异步适合:需要“等待外部资源”(IO密集型)——比如前端请求API、测试时调用链上合约、后端读取数据库(这些操作的“等待时间”远大于执行时间,异步能避免资源浪费)。

3. 同步语言也能“模拟异步”,异步语言也能“写同步代码”

  • 同步语言(比如Java):可以用“多线程+回调”模拟异步(比如用 Thread 开启新线程处理IO操作);
  • 异步语言(比如JS):如果所有代码都不用 await、不写回调,就是同步执行(比如简单的数学计算 1+1=2,不需要等待,直接执行)。

五、对你的学习启示(结合Solidity开发)

  1. Solidity 永远是同步的,不用纠结“异步合约”:链上执行必须保证确定性,所以合约代码的执行顺序、结果都是固定的,你写 Solidity 时,只需要关注“顺序逻辑”(比如先检查权限,再执行转账),不用考虑异步相关的问题;
  2. JS/TS 的异步是“工具层需求”:你用 JS 写测试、写DApp前端时,之所以需要异步,是因为要和“链上资源”“文件”“网络”交互(这些都需要等待),掌握 Promise+async/await 就够了,不用深入复杂的异步原理;
  3. 选择同步还是异步,看“是否需要等待”:比如你写合约函数(同步,不需要等待外部资源),写测试脚本(异步,需要等待链上操作)——两者是“互补”的,不是“对立”的。

总结

同步和异步的核心区别,本质是 “等待时是否浪费资源”和“执行顺序是否固定”

  • 同步:顺序固定、阻塞等待,适合需要确定性的场景(比如Solidity合约);
  • 异步:顺序灵活、非阻塞等待,适合IO密集型场景(比如JS测试、前端交互)。

对你来说,不用纠结“哪种语言更好”,而是记住:Solidity 写合约用同步逻辑,工具层(测试/前端)用JS的异步(async/await)处理等待场景——这正是你学习 Solidity 开发时会遇到的最常见情况,掌握这个搭配就够应对大部分工作了。