一、事件轮询机制(Event Loop):JS 异步执行的核心

JavaScript 是单线程语言,同一时间只能执行一个任务。为解决单线程下 “耗时任务阻塞后续代码” 的问题,JS 设计了 事件轮询机制,通过 “同步任务优先执行、异步任务排队等待” 的规则,实现非阻塞的代码执行。

1. 核心概念铺垫

在理解事件轮询前,需先明确以下基础概念:

概念定义关键说明
进程系统中正在运行的应用程序(如浏览器、Node.js)一个进程可包含多个线程,每个线程共享进程的内存空间
线程进程内的执行路径,是 CPU 调度的基本单位JS 主线程负责执行同步代码,另有专门线程处理异步任务(如定时器、网络请求)
单线程同一时间只能执行一个任务,任务需排队JS 设计为单线程的核心原因:避免多线程操作 DOM 时的冲突(如一个线程创建 DOM,一个线程删除 DOM)
任务队列存放待执行的异步任务的队列分为 “宏任务队列” 和 “微任务队列”,优先级不同

2. 事件轮询的执行流程

事件轮询的本质是 “主线程与任务队列的循环交互”,具体步骤如下:

  1. 执行同步任务:主线程优先执行所有同步代码,直到同步任务执行完毕(主线程为空)。
  2. 处理异步任务
    • 异步任务(如 setTimeoutPromise.then)触发时,先进入 Event Table 注册函数(记录 “任务完成后需执行的回调”)。
    • 当异步任务的触发条件满足(如定时器到期、Promise 状态改变),Event Table 会将回调函数移入对应的 任务队列(宏任务队列 / 微任务队列)。
  3. 清空微任务队列:主线程为空后,先检查 微任务队列,将队列中所有微任务依次执行完毕(微任务执行过程中产生的新微任务,也会在本轮清空)。
  4. 执行一个宏任务:微任务队列清空后,从 宏任务队列 中取出第一个宏任务,放入主线程执行。
  5. 循环重复:重复步骤 1-4,形成 “事件循环”。

3. 宏任务与微任务:异步任务的优先级

异步任务分为 宏任务 和 微任务,二者的执行优先级有明确区别:微任务优先级 > 宏任务优先级(同一轮事件循环中,微任务先于宏任务执行)。

3.1 宏任务(Macro Task)
  • 定义:由 “宿主环境”(浏览器 / Node.js)触发的异步任务,执行优先级较低。
  • 常见类型
    • setTimeoutsetInterval(定时器)
    • DOM 事件(如 clickload
    • 网络请求(如 Ajaxfetch
    • script 标签(整个脚本的执行属于第一个宏任务)
3.2 微任务(Micro Task)
  • 定义:由 “JS 引擎” 触发的异步任务,执行优先级较高。
  • 常见类型
    • Promise.then()Promise.catch()Promise.finally()(注意:new Promise(...) 内部的代码是同步的,仅回调函数是微任务)
    • async/awaitawait 后面的代码会被封装为微任务)
    • queueMicrotask()(专门用于添加微任务的 API)
3.3 执行顺序案例(关键!)

通过案例理解执行顺序:同步代码 → 所有微任务 → 一个宏任务

console.log(1); // 同步代码:1

// 宏任务:setTimeout 回调
setTimeout(function() {
    console.log(2); // 最后执行(宏任务优先级低)
}, 0);

// Promise:new Promise 内部是同步代码,then 回调是微任务
let pro = new Promise((resolve, reject) => {
    console.log(3); // 同步代码:2
    resolve(1000);  // 标记 Promise 为成功状态
    console.log(4); // 同步代码:3
});

// 微任务:Promise.then 回调
pro.then(data => {
    console.log(data); // 微任务:4(同步代码后执行)
});

console.log(5); // 同步代码:5

// 最终输出顺序:1 → 3 → 4 → 5 → 1000 → 2

二、闭包:延长变量生命周期的核心技巧

1. 闭包的引出:经典定时器问题

先看一个常见的 “坑”:使用 var 循环创建定时器,期望输出 0-4,实际输出 5 个 5。

for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i); // 输出:5 5 5 5 5(而非 0-4)
    });
}
console.log('a'); // 输出:a(同步代码优先)
问题原因:
  • var 声明的变量 没有块级作用域,循环中的 i 是全局变量,循环结束后 i 的值固定为 5。
  • setTimeout 是异步宏任务,回调函数会在 “同步代码(循环 + console.log ('a'))执行完毕后” 才执行,此时 i 已变为 5。

2. 闭包的定义与特性

2.1 定义

闭包(Closure)是能够访问另一个函数作用域中变量的函数。简单来说:当一个内层函数被外层函数返回并在外部调用时,内层函数依然能访问外层函数的局部变量,这就是闭包。

2.2 三大核心特性
  1. 函数嵌套函数:闭包一定存在函数嵌套结构(内层函数是闭包)。
  2. 内层函数引用外层函数的变量 / 参数:这是闭包的核心目的 ——“跨作用域访问变量”。
  3. 外层函数的变量不会被垃圾回收:由于内层函数引用了外层变量,JS 引擎会保留这些变量在内存中(延长变量生命周期),直到闭包不再被引用。

3. 闭包的实现与应用

3.1 解决 “定时器循环问题”(闭包方案)

通过 “自执行函数” 创建闭包,为每次循环的 i 生成独立的作用域,保存 i 的当前值:

for (var i = 0; i < 5; i++) {
    // 自执行函数:立即执行,将当前 i 作为参数传入,生成独立作用域
    (function(i) {
        setTimeout(function () {
            console.log(i); // 输出:0 1 2 3 4(闭包保存了每次循环的 i 值)
        });
    })(i); // 传入当前循环的 i 值
}
console.log('a'); // 输出:a(同步代码优先)
简化方案:用 let 替代 var

let 有块级作用域,每次循环会创建独立的 i,本质也是利用了闭包特性:

for (let i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i); // 输出:0 1 2 3 4(let 块级作用域替代闭包)
    });
}
3.2 闭包的核心作用:延长变量作用域

闭包可以让 “全局函数” 访问 “函数内部的局部变量”,且局部变量不会被回收:

function person(name) {
    // 外层函数的局部变量:name
    var name = name;
    
    // 内层函数(闭包):引用外层变量 name
    function sayName() {
        console.log(name);
    }
    
    return sayName; // 返回闭包
}

// 全局变量 fun 接收闭包
var fun = person('王小明');
fun(); // 输出:王小明(全局函数 fun 访问到 person 内部的 name)
3.3 解决 “DOM 点击事件下标问题”

需求:给 5 个 li 绑定点击事件,点击时输出当前 li 的下标(用 var 会导致所有点击输出 5,用闭包解决):

html 预览

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

<script>
    var oli = document.getElementsByTagName('li');
    
    // 闭包方案:自执行函数保存每次循环的 i
    for (var i = 0; i < oli.length; i++) {
        oli[i].onclick = (function(i) {
            // 返回点击回调函数(闭包),保存当前 i
            return function() {
                console.log(i); // 点击第 n 个 li,输出 n-1(0-4)
            }
        })(i);
    }
</script>

4. 闭包的注意事项

  • 内存消耗问题:由于闭包会保留外层函数的变量在内存中,滥用闭包会导致内存占用过高,甚至内存泄漏(如 IE 浏览器)。
  • 解决方案:在闭包不再使用时,手动将引用的变量设为 null,释放内存:
    function outer() {
        var num = 1;
        function inner() {
            console.log(num);
        }
        return inner;
    }
    
    var clo = outer();
    clo(); // 1
    clo = null; // 手动释放闭包引用,让 num 可被垃圾回收
    

三、自执行函数:创建临时作用域的工具

1. 定义与作用

自执行函数(Immediately Invoked Function Expression,IIFE)是定义后立即执行的函数,核心作用是:

  • 创建临时作用域,避免变量污染全局(早期没有 let/const 时,常用 IIFE 隔离作用域)。
  • 为闭包传递参数(如前面的定时器案例)。

2. 常见语法形式

自执行函数需满足 “函数表达式” 的语法(避免被解析为函数声明),常见写法如下:

// 1. 括号包裹函数表达式(最常用)
(function() { console.log('自执行1'); })();
(function() { console.log('自执行2'); }());

// 2. 一元运算符开头(利用运算符将函数转为表达式)
!function() { console.log('自执行3'); }();
+function() { console.log('自执行4'); }();
-function() { console.log('自执行5'); }();
~function() { console.log('自执行6'); }();

// 3. void 关键字开头
void function() { console.log('自执行7'); }();

// 4. new 关键字开头
new function() { console.log('自执行8'); }();
注意事项:
  • 若上一行代码未用分号 ; 结束,自执行函数的括号可能被解析为 “函数调用”,导致语法错误。因此建议在 IIFE 前加 ;
    // 安全写法:前加分号
    ;(function() { console.log('安全自执行'); })();
    
  • 自执行函数通常是匿名的(后续不再调用),也可命名,但命名仅在函数内部有效。

四、核心知识点总结

  1. 事件轮询机制:JS 单线程的异步解决方案,执行顺序为 “同步代码 → 所有微任务 → 一个宏任务”,循环往复。
  2. 宏任务 vs 微任务:微任务(如 Promise.then)优先级高于宏任务(如 setTimeout),同一轮事件循环先清空微任务。
  3. 闭包:能访问外层函数变量的内层函数,核心作用是 “跨作用域访问变量” 和 “延长变量生命周期”,需注意内存消耗问题。
  4. 自执行函数:定义后立即执行,用于创建临时作用域,常配合闭包传递参数,避免全局变量污染。

更多推荐