JavaScript 基础理解一

变量

变量是可变的量。将编程思想转换为现实生活中的例子进行理解。可变的量存在一个容器中,就像一个苹果箱里面有着许多苹果,箱子的作用就是用于存放量,而里面的苹果就是实际的值。

如:var apples = 20

var: 相当于制作了一个空箱子

apples: 给这个空箱子贴上苹果的标签,用于识别里面存放的是什么

20: 箱子里放了20个苹果

apples = 30 把20个苹果拿走,换成30个(重新赋值)

整个过程就像是工厂制作好箱子贴上标签,放入苹果,等待客户过来订单拿走。由此可以理解变量的本质:计算机内存中一块有名字的存储空间("箱子"),变量名是 "用于方便识别的标签",变量值是 "箱子里的东西";

var/let

制作空箱子的方式有两种var和let,这两个关键字来声明变量。通过var或let制作出一个空箱子,贴上用于识别的标签。

csharp 复制代码
声明变量语法: var 变量名;  或 let 变量名; 

两者的区别:核心差异集中在作用域、变量提升、重复声明、全局绑定

1.作用域:var 是 "函数 / 全局作用域",let 是 "块级作用域"。

var 无视块级作用域,只认 "函数" 或 "全局" 边界,会从块内 "泄露" 到外部,污染全局。

块级作用域 :就是 {} 包裹的区域(比如 ifforwhile 或直接写的 {}),let 声明的变量只在当前块内有效,出了块就 "消失",避免了全局污染。

  1. 变量提升:var 完全提升,let 提升但有 "暂时性死区",变量提升:JS 引擎会把变量声明 "提前" 到作用域顶部,但初始化(赋值)还在原来的位置。
  • var:声明 + 初始化都被提升(提前造了箱子,还往里面放了 "空"),声明前访问不会报错,只会得到 undefined

  • let:只有声明被提升,但初始化未完成,声明前访问会报错(这个阶段叫 "暂时性死区")------ 相当于 "提前说要造箱子,但箱子还没做好,不能用"。

  1. 重复声明:var 允许,let 禁止
  • var:同一作用域内可以重复声明同一个变量(相当于给同一个箱子反复贴标签,不会报错);

  • let:同一作用域内禁止重复声明(同一个区域不能有两个贴一样标签的箱子,会直接报错)。

  1. 全局作用域绑定:var 挂到 window,let 不挂,在全局作用域(函数外)声明变量时:
  • var 声明的变量会成为 window 对象的属性(相当于把箱子直接挂在 "房子" 墙上,所有人都能看到);
  • let 声明的变量不会绑定到 window(箱子放在房子的公共区域,但不挂墙,不属于房子的属性)。

1.作用域

// 用 let 声明(块级作用域)

ini 复制代码
{ 
   let apple1 = 10;
   console.log(apple1);   //输出10
}

console.log(apple1);  // 报错:apple1 is not defined

生活中的例子来理解:

块级作用域 = 超市的 "分区管理"

  • {} 就对应超市里的水果区(一个独立的块);

  • let apple1 = 10 就是 "水果区专属的苹果箱",这个箱子被明确规定 "只能在水果区范围内";

  • 出了水果区(也就是 } 之后),到蔬菜区 / 日用品区(块外部),自然找不到这个 "水果区专属箱子",所以 console.log(apple1) 会报错。箱子里的苹果只能在水果区域。不能在蔬菜或其他日用品区域出现。进行了规定及区域限制。

再举一个例子:

javascript 复制代码
// 只有上午10点-11点(条件满足),试吃区(块)才开放 
if (new Date().getHours() >= 10 && new Date().getHours() < 11) { 
    let trialApple = 1; 
    console.log(trialApple); // 试吃区能拿,输出1 
  } 
  // 过了11点离开试吃区,就拿不能拿到试吃的食物
  console.log(trialApple);  // 报错:trialApple is not defined

对比 var(无块级作用域)= 小卖部的随意摆放

javascript 复制代码
{ 
    var apple2 = 20; 
    console.log(apple2); //输出20

 }

console.log(apple2) //输出20

var 声明的变量就像小卖部没有区域区分和限制,不管是水果区、日用品区,整个小卖部(函数 / 全局作用域)都能用到。

典型场景:for 循环

css 复制代码
// var 版:循环结束后 i 会泄露,且所有循环体共享同一个 i 
for (var i = 0; i < 3; i++) { 
    setTimeout(() => console.log(i), 100);
    // 输出 3、3、3(因为共享一个i,最后i=3) 
 } 


// let 版:每次循环都会创建新的 i,块级作用域隔离 
for (let i = 0; i < 3; i++) { 
    setTimeout(() => console.log(i), 100); // 输出 0、1、2(每个循环有自己的i) 
}

进行分析var版:

  • 同步代码优先执行:for 循环是 "同步代码",需要从头到尾跑完。

  • 异步代码延后执行setTimeout 是 "异步代码",要等同步代码全部跑完,且等待 100ms 后才执行。

  • var 声明的 i 是 "共享的" :var 没有块级作用域,整个 for 循环里只有一个 i 变量(相当于一个公共的本子),循环中每次修改的都是这个本子上的数字。

步骤 1:初始化变量(同步)

执行 var i = 0:创建一个全局 / 函数作用域的变量 i,值为 0(公共本子上先写 0)。

步骤 2:第一次循环(同步)
  • 判断条件 i < 3:0 < 3,条件成立;
  • 执行循环体:调用 setTimeout,把回调函数 () => console.log(i) 放入 "异步任务队列",此时回调函数还没执行
  • 执行 i++:把公共本子上的 i 改成 1。
步骤 3:第二次循环(同步)
  • 判断条件 i < 3:1 < 3,条件成立;
  • 执行循环体:再放一个回调函数到异步队列;
  • 执行 i++:公共本子上的 i 改成 2。
步骤 4:第三次循环(同步)
  • 判断条件 i < 3:2 < 3,条件成立;
  • 执行循环体:放第三个回调函数到异步队列;
  • 执行 i++:公共本子上的 i 改成 3。
步骤 5:循环结束(同步)
  • 判断条件 i < 3:3 < 3,条件不成立,for 循环彻底跑完;
  • 此时同步代码全部执行完毕,公共本子上的 i 固定为 3。
步骤 6:执行异步回调(延后)

等待 100ms 后,JS 依次执行异步队列里的 3 个回调函数:

  • 第一个回调:去查公共本子上的 i → 3,输出 3;
  • 第二个回调:还是查同一个公共本子 → 3,输出 3;
  • 第三个回调:依旧查这个本子 → 3,输出 3。

核心原因:var 声明的 i全局 / 函数作用域 ,整个循环只有一个 i,同步循环跑完后 i 已经变成 3

进行分析let版:

let 在 for 循环中有个特殊设计 ------每次循环迭代都会创建一个全新的、独立的 i 变量 (而非共享同一个),每个 setTimeout 回调会 "绑定" 当前迭代的这个独立 i

步骤 1:第一次循环迭代(同步执行)
  1. 创建第一个独立的 i 变量(块级作用域),初始值为 0;
  2. 判断条件 i < 3(0 < 3,成立);
  3. 执行 setTimeout:把回调函数 () => console.log(i) 放入异步队列,这个回调会 "记住" 当前这个独立的 i=0
  4. 执行 i++:本次迭代的 i 变成 1(但这个变化只属于当前迭代的独立 i)。
步骤 2:第二次循环迭代(同步执行)
  1. 创建第二个独立的 i 变量(全新的,和上一个无关),初始值继承上一次的结果(1);
  2. 判断条件 i < 3(1 < 3,成立);
  3. 执行 setTimeout:回调 "记住" 当前这个独立的 i=1,放入异步队列;
  4. 执行 i++:本次迭代的 i 变成 2。
步骤 3:第三次循环迭代(同步执行)
  1. 创建第三个独立的 i 变量,初始值为 2;
  2. 判断条件 i < 3(2 < 3,成立);
  3. 执行 setTimeout:回调 "记住" 当前这个独立的 i=2,放入异步队列;
  4. 执行 i++:本次迭代的 i 变成 3。
步骤 4:循环终止(同步执行)

判断条件 i < 3(3 < 3,不成立),for 循环彻底跑完。

步骤 5:执行异步回调(延后执行)

100ms 后,JS 依次执行异步队列里的 3 个回调函数:

  • 第一个回调:调用 "记住" 的第一个 i=0 → 输出 0;
  • 第二个回调:调用 "记住" 的第二个 i=1 → 输出 1;
  • 第三个回调:调用 "记住" 的第三个 i=2 → 输出 2。

核心运行逻辑差异:

var 整个循环只有1 个共享的 i (函数 / 全局作用域),let 每次循环创建新的独立 i(块级作用域)。

var 无块级作用域,i 泄露到循环外,let 块级作用域,每个 i 仅限当前迭代使用。

var回调绑定 "唯一的共享 i",最终取到 i=3。let绑定 "当前迭代的独立 i",分别是 0/1/2。

2. 暂时性死区

javascript 复制代码
// var 提升:提前造了箱子,里面是空的 
console.log(banana); // 输出 undefined(箱子存在但没装苹果) 
var banana = 15; // 声明+赋值 

// let 暂时性死区:箱子还没造好,不能用 
console.log(orange); // 报错:Cannot access 'orange' before initialization 
let orange = 25; // 声明+赋值

3.重复声明

ini 复制代码
// var 重复声明:没问题 
var pear = 5; 
var pear = 8; // 覆盖之前的值,不会报错 
console.log(pear); // 输出 8 

// let 重复声明:报错 
let grape = 6; 
let grape = 9; // 报错:Identifier 'grape' has already been declared

4.var 挂到 window,let 不挂

javascript 复制代码
// 全局 
var mango = 30;
console.log(window.mango); // 输出 30(挂在window上) 

// 全局 
let cherry = 40; 
console.log(window.cherry); // 输出 undefined(不挂在window上)

var变量提升

代码是从上一行一行往下执行

javascript 复制代码
//执行顺序声明一个变量num 并赋值为20 
var num = 20
console.log(num) //再打印输出这个变量的值


//根据代码从上往下执行,sun输出时没有声明变量,应该报错,但是输出的是undefined
console.log(sun)
var sun = 30   //是因为浏览器会将var sun 放到最顶部,变量提升

//如下形式
var sun;
console.log(sun);
sun = 30;

const

const 声明的变量,指向的内存地址不可变(简单说就是 "箱子不能换,但箱子里的内容可能能改")

  1. 声明时必须初始化(不能造 "空箱子")

const 声明变量时,必须立刻赋值(往箱子里放东西),不能像 let/var 那样先声明、后赋值,否则直接报错。

错误:const 不能声明空变量 const apple;

正确:声明时必须初始化 const apple = 10;

2.声明后不能给 const 变量重新赋值(相当于不能把整个箱子换成新的),否则报错

const total = 20; total = 30;// 错误:不能重新赋值(换箱子)

  1. 块级作用域(和 let 完全一致)

在所在的 {} 块内有效,出块即失效

4.引用类型(对象 / 数组):内容可改,指向不可改

const 绑定的是简单类型(数字、字符串、布尔值),因为值直接存在 "箱子" 里,指向不可变 = 值不可变;

如果绑定的是复杂类型(对象、数组),"箱子" 里装的是 "指向果篮的地址",地址不可改(不能换果篮),但果篮里的内容可以改。

ini 复制代码
// 简单类型(值不可变) 
const num = 10; 
num = 20; // 报错(换箱子=改值) 

// 复杂类型(对象)------ 内容可改,指向不可改 
const fruitBasket = { red: 10, green: 5 }; // 可以改箱子里的内容(调整果篮里的苹果数量) fruitBasket.red = 15;
console.log(fruitBasket.red); // 输出 15 
fruitBasket = { orange: 8 }; //不能换箱子(不能改指向的地址) 报错

// 示复杂类型(数组)
const arr = [1, 2, 3]; 
arr.push(4); //可以改内容,输出 [1,2,3,4] 
arr = [5,6]; //  不能换数组(改指向),报错
  • const 核心是 "指向不可变",而非 "值不可变"------ 简单类型值不可改,复杂类型内容可改、指向不可改;

  • const 声明必须初始化、不可重新赋值、有块级作用域,禁止重复声明;

数据类型

js中的数据类型,将编程思维变成生活中思维可以理解成归类,用于更加快捷,方便的区分,通过统一标签降低代码混乱,根据其特性进行使用。

比如将水果和蔬菜放在一个大筐里,想要从里面拿出一个苹果,需要在一堆各种各样的水果和蔬菜混装中找到,不方便。如果一次性要找出五个苹果,那么花费的时间更长。

但将水果放在一个大筐里,蔬菜单独放在一个大筐里,这样比较好找一些。如果再细分下,划分两个区域,一个区域放水果,苹果单独一筐,香蕉单独一筐。另一个区域放蔬菜,青菜单独一筐,胡萝卜单独一筐,这样既不混乱也方便找到需要的东西。

同时蔬菜和水果不能进行炒菜,这样也区分了特性。

基于上面的理解,那么js数据类型也可以分为两个大区域:基本类型和引用类型。为了更好使用分别又进行了划分,7种基本数据类型,引用类型Object

1.基本数据类型(原始类型)

基本数据类型:值直接存在变量指向的内存地址(箱子里直接装东西),箱子里直接装苹果、香蕉(值),拿取直接用

1.String 字符串

定义:文本内容,用单引号 / 双引号 / 反引号包裹;

ini 复制代码
const fruitName = "苹果"; // 双引号 
const desc = '红富士苹果'; // 单引号 
const priceDesc = `苹果单价:8.99元`; // 反引号
  1. Number 数字

定义:包含整数、小数、特殊值(NaN、Infinity);

ini 复制代码
const appleCount = 20; // 整数 
const applePrice = 8.99; // 小数 
const invalidNum = 10 / "苹果"; // NaN(Not a Number,非数字,注意:NaN 不等于任何值,包括自己) 
const bigNum = 1 / 0; // Infinity(无穷大)
  1. Boolean 布尔值

定义 :只有两个值:true(真)、false(假),用于条件判断;

  1. Undefined 未定义

定义 :变量声明了但未赋值时的默认值;

javascript 复制代码
let apple; // 只声明,没赋值 
console.log(apple); // 输出 undefined
  1. Null 空值

定义:主动声明的 "空",表示变量指向的内存地址无内容;

csharp 复制代码
const emptyBox = null; // 主动表示箱子是空的
  1. Symbol 符号

定义:唯一的、不可重复的值,用于创建唯一标识;

ini 复制代码
const id1 = Symbol("apple"); 
const id2 = Symbol("apple"); 
console.log(id1 === id2); // 输出 false
  1. BigInt 大整数

定义 :解决 Number 的精度问题,处理超大整数,后缀加 n,不能和 Number 直接运算,需先转换;

ini 复制代码
const bigNum = 9007199254740993n; // 大整数 
const sum = bigNum + 1n; // 运算时也要加 n,输出 9007199254740994n

2.引用数据类型

  1. Object 普通对象

定义:键值对(key-value)集合,key 是字符串 / Symbol,value 可以是任意类型;

arduino 复制代码
const fruit = { 
    name: "苹果", // key: name,value: 字符串
    price: 8.99, // key: price,value: 数字 
    hasStock: true // key: hasStock,value: 布尔值 
    }; 
    // 修改对象内容(允许,因为只是改地址指向的内容) 
    fruit.price = 7.99; 
    console.log(fruit.price); // 输出 7.99
    
  1. Array 数组

定义:有序的集合,索引从 0 开始,本质是特殊的 Object;

perl 复制代码
const fruits = ["苹果", "香蕉", "橙子"]; // 修改数组内容(允许) 
fruits.push("葡萄"); // 新增元素 
console.log(fruits); // 输出 ["苹果", "香蕉", "橙子", "葡萄"]
  1. Function 函数

定义:可执行的代码块,本质是特殊的 Object(可以作为参数、返回值);

  1. 其他引用类型
  • Date(日期):处理时间,const now = new Date();

  • RegExp(正则):处理字符串匹配,const reg = /apple/;

堆和栈

  • 栈(Stack,执行栈 / 调用栈) :像取餐口 ------ 空间小、存取快、顺序先进后出,只能放固定大小的物品。

  • 堆(Heap) :像仓库 ------ 空间大、能放大小不固定的物品,存取稍慢,物品位置无序,需要标记(地址)才能找到。

JS 引擎正是通过这两个空间的配合,完成所有数据的存储和管理。

  • 堆的内存不会自动释放,需要 JS 的垃圾回收机制(GC)定期清理无引用的对象;

  • 堆中的数据没有固定顺序,每个数据会有一个「内存地址」(指针),通过这个地址才能找到数据。

    ini 复制代码
    // 1. 堆中创建对象本体:{ name: "张三" },分配地址(比如 0x123) 
    // 2. 栈中存储:obj1 → 0x123(指针指向堆的地址0x123) 
    let obj1 = { name: "张三" }; //将地址赋值给到变量,变量拿到的是地址而非真正的值
    // 3. 栈中拷贝指针:obj2 → 0x123(obj1和obj2指向堆中同一个对象) 
    let obj2 = obj1; 
    // 4. 通过obj2修改堆中的数据本体 
    obj2.name = "李四"; 
    // 5. obj1通过指针访问堆中同一数据,所以值也变了 
    console.log(obj1.name); // 输出 李四
  • 赋值阶段let obj1 = { name: "张三" }

  • JS 引擎先在堆内存 里开辟一块空间,存入 { name: "张三" } 这个对象本体,并给这块空间分配唯一的内存地址(比如0x123); - 然后在栈内存 里创建变量 obj1,并把「地址 0x123」这个指针赋值给 obj1 ------ 所以 obj1 本身存的不是对象,而是指向对象的 "门牌号"。

  • 拷贝阶段let obj2 = obj1

    • 这一步并不是把堆里的对象复制一份,而是把栈里 obj1 存的地址(0x123)拷贝给 obj2
    • 此时栈里 obj1obj2 都指向 0x123,相当于两个人拿着同一个门牌号,能找到同一个房子(堆里的对象)。
  • 修改阶段obj2.name = "李四"

    • 引擎先读取栈里 obj2 的地址(0x123),然后根据这个地址找到堆里的对象;
    • 直接修改堆里这个对象的 name 属性 ------ 因为房子只有一个,不管用哪个门牌号进去改,房子里的东西都会变。
  • 访问阶段console.log(obj1.name)

    • 引擎读取栈里 obj1 的地址(0x123),找到堆里的对象,读取 name 属性 ------ 自然就是修改后的「李四」。

代码2:

ini 复制代码
  let obj1 = { name: "张三" }; 
  let obj2 = obj1; // 注意:这是给obj2重新赋值,不是修改属性 obj2 = { name: "王五" };
  console.log(obj1.name); // 输出 张三(而非王五)
  • 堆内存:有一个对象 { name: "张三" },地址 0x123

  • 栈内存:obj1 → 0x123obj2 → 0x123(两个变量都指向同一个堆地址)

  • JS 引擎看到你写了 { name: "王五" } ------ 这是一个「全新的对象字面量」,引擎会默认认为你需要一个新对象,因此会在堆里重新开辟一块新空间(比如地址 0x456),并把 { name: "王五" } 存入这个新地址;

  • 修改栈里的指针 :把栈中 obj2 原来存储的地址 0x123 替换成新地址 0x456; 此时 obj1 仍指向 0x123(原堆对象),obj2 指向 0x456(新堆对象);堆里同时存在 0x1230x456 两个独立的对象,互不影响。

  • JS 中只要写 {}/[]/function(){} 等引用类型字面量 ,引擎就会在堆里新建一块空间存储这个新数据;所以obj2 = { name: "王五" } 是 "赋值新对象",而非 "修改原对象",所以会先创建新堆地址,再更新栈里 obj2 的指针;

基本数据类型放在栈中

基本类型放在栈里,是 JS 引擎为了「性能最优」做的设计,栈的存取速度远高于堆,栈内存的核心特征之一是:只能存储「大小固定、已知」的数据

栈是 "先进后出" 的线性结构,数据的存入(压栈)、取出(弹栈)只需要操作栈顶指针,不需要像堆那样遍历、查找内存地址,CPU 能直接缓存栈的连续内存,访问速度极快;

基本类型是 JS 中使用最频繁的数据(比如数字计算、布尔判断、简单字符串拼接),把它们放在最快的栈里,能最大程度减少内存访问耗时,提升代码执行效率。

如果把基本类型放堆里,每次访问都要先查栈里的指针,再找堆里的数据。

栈是一块连续的线性内存空间,像一排编号固定的小格子。7 种基本数据类型(Number、String、Boolean、Undefined、Null、Symbol、BigInt),它们的值在创建时大小就是固定的:

javascript 复制代码
-   Number:不管是 10 还是 100000,都占用 8 字节(JS 中统一用 64 位浮点数存储);
-   Boolean:只有 true/false 两种可能,占用 1 字节;
-   String:虽然看起来长度可变,但 JS 中字符串是「不可变的」
-   Undefined/Null:占用极小且固定的内存空间。

引用类型(对象、数组等)大小不固定(比如数组可以无限 push 元素),无法提前确定占用多少字节,所以只能放在 "不限制大小、无序存储" 的堆里。

栈是自动释放:函数执行时,变量被压入栈;函数执行结束,对应的栈帧(包含变量)会立即被销毁,内存自动释放。

csharp 复制代码
// 执行函数时,栈中创建栈帧,存入a、b(固定大小,快速分配)
    function add() { 
        let a = 10; // 栈:a → 10 
        let b = 20; // 栈:b → 20 
        return a + b; 
    } 
add(); // 函数执行结束,栈帧被立即销毁,a、b的内存自动释放,无残留

同步和异步

  • 同步:像去奶茶店排队买奶茶 ------ 必须等前面的人都买完、你拿到奶茶,才能做下一件事,一步等一步,完全按顺序来;

  • 异步:像点外卖 ------ 下单后不用等外卖送到,你可以先去看电视,等外卖到了(有结果了),再处理收外卖这件事,不用全程等待。

同步(Synchronous):按顺序执行,阻塞线程

同步是 JS 代码的「默认执行模式」,核心规则是:代码严格按照书写顺序依次执行,前一行代码执行完成(不管是简单计算、函数调用),后一行代码才会开始执行

在执行同步代码时,JS 的主线程会被「阻塞」------ 直到当前同步任务完成,才能处理下一个任务。

为何要使用同步,是因为JS可修改DOM结构,JS和DOM共用一个线程。

2. 异步(Asynchronous):不等待,不阻塞线程

异步是为了解决「同步阻塞」问题设计的执行模式,核心规则是:耗时的异步任务不会阻塞主线程,JS 会先跳过它执行后面的同步代码,等异步任务有结果了(比如定时器到时间、网络请求返回),再回头执行对应的回调函数

javascript 复制代码
console.log('1. 主线程开始执行'); // 异步任务:定时器(延迟1秒执行回调)
setTimeout(() => {
    console.log('2. 异步定时器回调执行'); 
}, 1000); // 不会等定时器,直接执行这行同步代码 
console.log('3. 主线程继续执行,不等异步任务');

1. 主线程开始执行
3. 主线程继续执行,不等异步任务 
2. 异步定时器回调执行 // 1秒后才输出

梳理

  • JS 是单线程:同一时间确实只能执行一个任务;

  • 执行优先级:先同步,后异步:同步任务全部执行完,才会处理异步任务;

  • 异步不会 "插队":哪怕异步任务先 "准备好"(比如定时器设 0 秒),也得等同步任务全执行完才会运行。

  • 如果没有异步,单线程的 JS 面对任何耗时操作(比如网络请求、定时器)都会卡死,而异步的核心好处就是「不阻塞主线程,让程序 / 页面始终可交互,同时高效利用资源」。

  • 比如:点击 "加载数据" 按钮后,用异步请求数据,用户依然可以滚动页面、点击其他按钮,不会出现 "卡死";页面加载时异步加载图片 / 数据,用户能先看到页面骨架,再逐步加载内容,而非白屏等待。

  • 同步模式下,CPU 会在耗时操作(比如网络请求)期间 "空等"(因为要等服务器返回数据,CPU 没事可做);

  • 异步模式下,CPU 会把耗时操作交给浏览器 / Node 的异步模块(比如网络线程、定时器线程)处理,自己继续执行其他任务,直到异步任务完成后再回调 ------CPU 始终在干活,不会闲置

  • JS 是单线程,但异步能让 JS "看起来像同时处理多个任务"(伪并发)

  • 同时发起 3 个网络请求(用户信息、商品列表、分类列表),异步模块会并行处理这 3 个请求,谁先完成谁先回调,总耗时≈最慢的那个请求的时间(而非 3 个请求时间相加);如果是同步,总耗时 = 请求 1 + 请求 2 + 请求 3,效率极低

调用栈(同步任务区) :奶茶店的「制作台」------ 只能做一杯奶茶(单线程),按顺序做完一个,才能接下一个;JS 引擎扫描代码,把所有同步任务(比如变量赋值、console.log、普通函数)依次推入「调用栈」,逐个执行。

任务队列(异步任务区, 队列结构,先进先出) :奶茶店的「取餐叫号机」------ 异步任务(比如外卖单)不会直接进制作台,而是先在叫号机排队,等制作台空了(同步任务做完),再按顺序叫号处理;

事件循环(协调者) :奶茶店的「店员」------ 不停检查制作台(调用栈)是否空,空了就去叫号机(任务队列)取一个异步单来做。

同步任务在「调用栈」执行,异步回调在「任务队列」排队,由「事件循环」协调执行。

执行异步任务:只有当「调用栈为空」(所有同步任务都执行完),事件循环才会把任务队列里的异步回调函数逐个推入调用栈执行 ------ "同步执行结束后,找到异步执行"。

思考的问题:当异步任务未完成是否影响到下一个异步任务。

JS 的任务队列是「先进先出」的独立队列,每个异步任务的回调都是独立排队、独立执行的。异步任务只要 "有结果了(不管是好结果还是坏结果)",对应的回调就会被放进任务队列,等调用栈空了执行;只有异步任务 "没完成"(比如网络请求还在 pending、定时器还在计时),回调才不会入队。

一个异步回调执行失败(比如报错),JS 引擎只会终止当前这个回调的执行,调用栈清空后,依然会继续执行任务队列里的下一个异步回调;

一号顾客的奶茶做砸了(回调报错),只会重新给一号做(如果处理了错误),但二号、三号顾客的奶茶依然会按顺序做,不会因为一号砸了就停。所以单个异步的成功 / 失败(或回调报错),不会影响任务队列里其他异步回调的执行。

当 JS 主线程遇到多个异步任务时,会把它们分别交给对应的异步模块,这些异步模块是多线程的,能同时处理多个任务(比如一个定时器线程计时的同时,另一个网络线程发请求);

每个异步任务只有自己 "完成"(成功 / 失败)后,才会把回调放进任务队列;未完成的异步任务,只是在自己的线程里 "等待",不会占用主线程,也不会阻止其他异步模块的工作。

多个异步任务的执行顺序,由它们各自完成的时间决定(谁先完成谁先入队执行)。

问题思考:多个异步任务按 "完成时间先到先得" 的方式执行,在需要「有序逻辑」的场景下是否造成影响

如果业务逻辑依赖固定执行顺序(比如先查用户、再查订单),会导致逻辑混乱、数据错误;如果业务逻辑不依赖顺序(比如同时加载两张无关的图片)。

先请求 "用户信息"(拿到用户 ID),再用用户 ID 请求 "用户订单"。如果订单请求网络更快,先完成入队执行,就会因为没有用户 ID 导致请求失败 / 数据错误。

javascript 复制代码
let userId = null;

 // 异步1:请求用户信息(假设网络慢,2秒完成) 
setTimeout(() => {
    userId = 1001; // 拿到用户ID
    console.log('异步1完成:拿到用户ID', userId); 
}, 2000); 

// 异步2:请求用户订单(依赖userId,假设网络快,1秒完成) 
setTimeout(() => { 
    console.log('异步2执行:请求订单,用户ID为', userId); // 此时userId还是null  
}, 1000);

异步2执行:请求订单,用户ID为 null ( 先完成的异步2先执行,拿到无效数据 )
异步1完成:拿到用户ID 1001

如何解决

让异步任务按「业务逻辑顺序」执行,而非「完成时间顺序」。异步执行顺序从 "时间驱动" 变回 "逻辑驱动";

方案 1:串行执行(依赖型异步,用 async/await)

"必须先 A 后 B" 的场景,让 B 等待 A 完成后再执行:

javascript 复制代码
 async function f() { 

     let userId = null; 
     
     await new Promise((resolve) => { 
         setTimeout(() => { 
             userId = 1001; 
             console.log('异步1完成:拿到用户ID', userId);
             resolve(); // 标记异步1完成 
         }, 2000); 
     }); 

     // 异步2:等异步1完成后再执行
     await new Promise((resolve) => { 
         setTimeout(() => { 
             console.log('异步2执行:请求订单,用户ID为', userId); // 此时ID=1001 
             resolve(); 
             }, 1000); 
         }); 
     } 

 f();
 

方案 2:并行等待(需要所有异步完成,用 Promise.all)

适合 "需要所有数据到齐再处理" 的场景,不管谁先完成,都等全部完成后统一执行:

javascript 复制代码
 // 异步1:请求商品列表(2秒完成) 
 const fetchGoods = new Promise((resolve) => { 
     setTimeout(() => { resolve(['商品1', '商品2']); }, 2000); 
  });

 // 异步2:请求分类列表(1秒完成) 
 const fetchCate = new Promise((resolve) => { 
     setTimeout(() => { resolve(['分类1', '分类2']); }, 1000); 
 }); // 等待所有异步完成,再统一处理 

 Promise.all([fetchGoods, fetchCate]).then(([goods, cate]) => { 
     console.log('所有数据到齐:', { goods, cate }); // 这里渲染页面,数据完整 
 });
 
 所有数据到齐: { goods: ['商品1', '商品2'], cate: ['分类1', '分类2'] }
 
 
 
 

promise

callback hell 回调地狱,在了解回调地狱时先了解下什么是回调。

回调 & 回调函数到底是什么?

你去蛋糕店定一个蛋糕,跟店员说:"蛋糕做好了叫我一声,我过来取"。

  • 这里的你就是程序主逻辑,而 "叫我一声" 这个动作就是回调函数,这件事情交给了店员(执行异步操作的函数);

  • 店员不用一直等蛋糕做好,忙别的事(异步执行),蛋糕做好了才会 "回头调用",执行"叫你" 这个动作;

  • 这个 "被交给别人、等时机到了再执行的动作",就是回调函数 ;"回头调用" 这个动作本身,就是回调

  • 回调(Callback) :指 "回头调用" 的行为 ------ 一个函数执行完成后,"回头" 调用另一个函数的过程。

  • 回调函数:被作为参数传递给另一个函数(我们称这个函数为 "主函数"),并由主函数在合适的时机(同步 / 异步操作完成后)调用执行的函数。

回调函数的本质是:把函数当作参数传递,让其他函数决定它的执行时机。

通过上面例子可以理解:

  • "我把函数给你,你用完了再叫我" :回调函数的执行权不在自己手里,而是交给了接收它的主函数;

  • 同步 / 异步都能用:异步场景(定时器、AJAX)是为了等结果,同步场景(forEach、sort)是为了自定义逻辑;

  • 本质是 "参数" :回调函数只是一个 "以函数形式存在的参数",和数字、字符串参数没有本质区别,只是类型不同。

回调函数的同步 / 异步,由执行回调的主函数决定:

  • 如果主函数在执行过程中立刻、无延迟 地调用回调函数 → 这是同步回调
  • 如果主函数先执行完,等某个异步操作(定时器、网络请求、文件读取)完成后延迟 调用回调函数 → 这是异步回调

async/await

异步编程在一些业务逻辑下存在问题,async/await是一种解决方案。

核心作用是把 "回调式 / 链式" 的异步代码改写成 "同步风格" ,大幅提升异步代码的可读性、可维护性,同时简化错误处理和异步顺序控制。

  1. 回调地狱:多层异步嵌套(比如 "请求用户→请求订单→请求订单详情"),代码缩进层层嵌套,可读性极差;
  2. Promise.then 链 :虽然解决了回调地狱,但多步异步会形成长长的.then链式调用,逻辑分散,依然不够直观;
  3. 错误处理繁琐 :Promise 需要用.catch单独捕获错误,多层异步的错误处理会分散在不同位置。

async/await正是为解决这些问题而生 ------ 让异步代码 "看起来像同步代码",同时保留异步非阻塞的特性。

相关推荐
mCell6 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell7 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭7 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清7 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木8 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076608 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声8 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易8 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得08 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
anOnion8 小时前
构建无障碍组件之Dialog Pattern
前端·html·交互设计