前端面试复习笔记:JS 基础核心知识点梳理
引言
作为前端开发的 "基石",JavaScript 基础原理总能在面试中 "出其不意"------ 事件循环到底怎么转?闭包为什么会导致内存泄漏?ES6 新特性在项目里怎么用才优雅?最近复习时把这些高频考点系统捋了一遍,整理成笔记分享出来,帮你搞定 JS 基础盲区,面试时更有底气~
真正的前端高手,都是把基础原理嚼透了的人 ------JS 基础就像内功,练扎实了才能应对各种复杂场景。
开始
📚 JavaScript 基础是前端开发的核心内功,也是面试中永恒的重点。这篇笔记聚焦JS 基础的 12 个关键模块,从运行机制到设计模式,覆盖日常开发与面试高频考点:
序号 | 核心模块 | 学习价值 |
---|---|---|
1 | JS 运行机制 | 理解代码执行的底层逻辑 |
2 | 事件循环队列执行过程 | 搞懂异步代码的执行顺序 |
3 | 全局执行上下文 | 掌握变量提升与作用域规则 |
4 | 闭包补充说明 | 解析闭包的应用与风险 |
5 | ES6 新特性 | 熟悉现代 JS 的语法糖与新能力 |
6 | AMD、ESModule 和 CommonJS 的区别 | 理清模块化规范的使用场景 |
7 | 深拷贝和浅拷贝的区别 | 解决对象复制的常见坑点 |
8 | 定时器 | 掌握 setTimeout/setInterval 的特性与问题 |
9 | requestAnimationFrame 与 setTimeout、setInterval 的区别 | 优化动画与定时任务的执行效率 |
10 | call、apply、bind 的核心作用 | 灵活操控函数的 this 指向 |
11 | JS 设计模式 | 用优雅的方式解决重复出现的问题 |
🔍 这些知识点看似基础,却藏着很多容易忽略的细节。比如事件循环的微任务优先级、闭包与垃圾回收的关系,都是面试中常被追问的点。接下来就逐一拆解这些内容,帮你把基础打牢~
介绍
1:JS运行机制
一、核心概念梳理
1. 函数执行栈(Call Stack)
-
本质:JS 引擎用于执行代码的 "单线程工作区",遵循后进先出(LIFO) 原则(而非 "先进先出")。
-
工作方式:
- 执行同步代码时,全局执行上下文(Global Context)先入栈,随后函数调用按 "调用顺序" 入栈(如
a()
调用b()
,b()
入栈后在栈顶,优先执行)。 - 函数执行完毕后出栈,直到全局执行上下文出栈(此时调用栈为空)。
- 执行同步代码时,全局执行上下文(Global Context)先入栈,随后函数调用按 "调用顺序" 入栈(如
-
作用:所有代码(同步代码、异步任务的回调)最终都需通过执行栈执行,是 JS "单线程" 特性的直接体现。
2. 宏任务(MacroTasks)
- 定义:由宿主环境(浏览器 / Node.js)发起的异步任务,执行优先级较低,每次事件循环仅执行一个宏任务。
- 常见类型:
环境 | 宏任务类型 |
---|---|
浏览器 | script (整体代码,第一个宏任务)、setTimeout 、setInterval 、I/O (如网络请求)、UI渲染 、MessageChannel 、requestAnimationFrame (与渲染帧同步) |
Node.js | setTimeout 、setInterval 、I/O 、setImmediate 、close 事件(如文件关闭) |
- 特点:宏任务执行前,会先清空所有微任务(保证微任务优先于下一个宏任务执行)。
3. 微任务(MicroTasks)
- 定义:由 JS 引擎自身发起的异步任务,执行优先级高于宏任务,每次宏任务执行后会清空所有微任务(包括执行过程中新增的微任务)。
- 常见类型:
环境 | 微任务类型 |
---|---|
浏览器 | Promise.then/catch/finally 、MutationObserver (监听 DOM 变化)、queueMicrotask (显式添加微任务) |
Node.js | Promise.then/catch/finally 、process.nextTick (优先级高于其他微任务)、queueMicrotask |
- 特点 :微任务执行时若产生新的微任务(如在
then
中再次调用queueMicrotask
),会被加入当前微任务队列末尾,同批次执行(不会等待下一个宏任务)。
4. 事件循环(Event Loop)
-
本质:JS 单线程下协调 "执行栈""宏任务队列""微任务队列" 的机制,保证异步任务有序执行。
-
核心目标:在单线程限制下,通过 "循环取任务 - 执行" 的方式,实现非阻塞的异步操作(如网络请求、定时器)。
二、完整执行流程(以浏览器为例)
- 初始化:第一个宏任务(
script
整体代码)- 全局执行上下文入栈,开始执行
script
中的同步代码:- 同步代码直接在执行栈中执行,执行完后出栈。
- 遇到函数调用:函数执行上下文入栈,执行完后出栈(遵循 LIFO)。
- 遇到宏任务(如
setTimeout
):回调函数被添加到 "宏任务队列"(等待执行)。 - 遇到微任务(如
Promise.then
):回调函数被添加到 "微任务队列"(等待执行)。
- 全局执行上下文入栈,开始执行
- 第一次宏任务执行完毕
script
同步代码执行完,全局执行上下文出栈,执行栈为空。
- 清空微任务队列
- 从微任务队列中依次取出所有回调,入栈执行(包括执行过程中新增的微任务,全部执行完才停止)。
- 例:若
then
回调中调用queueMicrotask(() => { ... })
,新微任务会被加入当前队列,同批次执行。
- 执行 UI 渲染(浏览器特有)
- 微任务队列清空后,浏览器会判断是否需要渲染(如 DOM 有变化),执行一次 UI 渲染(与
requestAnimationFrame
回调配合)。
- 微任务队列清空后,浏览器会判断是否需要渲染(如 DOM 有变化),执行一次 UI 渲染(与
- 循环执行:取下一个宏任务
-
从宏任务队列中按 "先进先出(FIFO)" 取第一个任务(如
setTimeout
回调),入栈执行。 -
重复步骤 2-4:该宏任务的同步代码执行完→执行栈为空→清空微任务队列→(可选)UI 渲染→取下一个宏任务...
-
三、关键细节补充
1.宏任务与微任务的优先级差异
-
微任务优先级 高于 宏任务:同一轮事件循环中,微任务队列会在 "当前宏任务执行完后立即清空",再执行下一个宏任务。
-
例:
jsconsole.log('同步代码'); // 同步执行 setTimeout(() => console.log('宏任务1'), 0);// 宏任务队列 Promise.resolve().then(() => console.log('微任务1')); // 微任务队列 // 执行顺序:同步代码 → 微任务1 → 宏任务1
2. 执行栈与异步任务的关系
-
所有异步任务(宏任务 / 微任务)的回调函数,必须等待执行栈为空时才会入栈执行。
-
例:若同步代码执行 100ms,即使setTimeout延迟 0ms,其回调也需等 100ms 后(执行栈空)才会执行。
3. 浏览器与 Node.js 的差异
-
宏任务队列:
- 浏览器:requestAnimationFrame与渲染帧绑定(每帧执行一次),UI渲染在微任务后执行。
- Node.js:setImmediate与setTimeout(fn, 0)的顺序不确定(取决于事件循环阶段)。
-
微任务队列:
- Node.js 中process.nextTick优先级高于其他微任务(会在所有Promise.then前执行)。
4. 微任务的 "连续执行" 特性
-
微任务执行过程中产生的新微任务,会被加入当前队列并立即执行,直到队列完全清空。
-
例:
jspromise.resolve().then(() => { console.log('微任务1'); Promise.resolve().then( () => console.log('微任务2')); // 新增微任务 }); // 执行顺序:微任务1 → 微任务2(同批次执行)
5.调用栈的 "后进先出" 特性
-
函数调用栈是 "栈结构",最后入栈的函数先执行(弹出)。
-
例:
jsfunction a() { b(); } function b() { c(); } function c() { console.log('c' ); } a(); // 调用栈变化:a入栈 → b入栈 → c入栈 → c出栈 → b出栈 → a出栈// 输出:c(c先执行)
四、总结
JS 运行机制的核心是 "单线程 + 事件循环" :
- 同步代码通过 "执行栈(LIFO)" 立即执行;
- 异步任务(宏 / 微)被放入对应队列,等待执行栈为空;
- 事件循环按 "1 个宏任务→所有微任务→(可选)UI 渲染" 的顺序循环,保证异步任务有序执行。
理解这一机制,能清晰解释 "为什么setTimeout
回调会延迟""为什么微任务比宏任务先执行" 等问题,是优化异步代码(如避免长时间阻塞执行栈)的基础。


2:事件循环队列执行过程
一、初始化:执行栈与全局执行上下文
-
执行栈创建
JS 引擎执行代码前,会创建一个执行栈(Call Stack),用于管理执行上下文(代码执行的环境)。执行栈是 "后进先出(LIFO)" 的栈结构,每次函数调用会入栈,执行完后出栈。
-
全局执行上下文入栈
当开始执行一段脚本(如
<script>
标签内的代码),JS 引擎首先创建全局执行上下文(Global Execution Context),并将其推入执行栈(此时执行栈只有全局上下文)。全局执行上下文的创建分两个阶段:
-
创建阶段:
- 进行 "变量提升":为所有
var
变量、函数声明分配内存,初始值设为undefined
(函数声明会直接赋值为函数体,优先于变量提升)。 - 确定
this
指向(浏览器中指向window
,Node.js 中指向global
)。 - 构建作用域链:全局作用域作为最外层链(后续函数调用会在作用域链中添加自身作用域)。
- 进行 "变量提升":为所有
-
执行阶段 : 逐行执行代码,为变量赋值(替换
undefined
为实际值),执行函数调用等同步操作。
-
二、同步执行:函数调用与执行上下文入栈
当执行到函数调用时(如fn()
),JS 引擎会创建函数执行上下文(Function Execution Context),并推入执行栈(位于栈顶,优先执行)。函数执行上下文的处理与全局上下文类似,但多了两个步骤:
-
创建阶段:
- 变量提升:为函数内的
var
变量、let
/const
变量(暂存死区)、函数参数、内部函数声明分配内存。 - 确定
this
指向(取决于调用方式,如普通调用指向window
,对象调用指向该对象)。 - 构建作用域链:以 "当前函数作用域→父函数作用域→...→全局作用域" 的顺序链接(这是子函数能访问父函数变量的原因)。
- 变量提升:为函数内的
-
执行阶段:
逐行执行函数内代码,为变量赋值,处理函数内的同步逻辑;若遇到嵌套函数调用(如
fn()
内调用childFn()
),则重复上述步骤,将childFn
的执行上下文推入栈顶。
三、执行栈清空:同步代码执行完毕
当所有同步代码(包括嵌套函数)执行完毕后,执行栈会依次弹出已完成的执行上下文(从栈顶的函数上下文开始,最后弹出全局上下文),此时执行栈为空。
四、异步任务:宿主环境处理与回调入队
同步执行过程中若遇到异步任务 (如setTimeout
、fetch
、Promise.then
),JS 引擎(单线程)不会阻塞等待,而是将异步逻辑交给宿主环境(浏览器 / Node.js)的其他线程处理(如浏览器的定时器线程、网络线程),具体流程:
- 异步任务触发 :
- 若为宏任务 (如
setTimeout
、fetch
):宿主线程(如定时器线程)会等待触发条件(如延迟时间到、请求响应),满足后将回调函数放入宏任务队列(按 "先进先出" 排序)。 - 若为微任务 (如
Promise.then
、queueMicrotask
):宿主线程处理完同步逻辑后,将回调函数放入微任务队列(优先级高于宏任务队列)。
- 若为宏任务 (如
- 回调入队的时机 :
-
Promise.then
:当Promise
状态变为fulfilled
或rejected
时,回调立即入微任务队列。 -
setTimeout
:延迟时间到后,回调入宏任务队列(即使延迟 0ms,也需等待当前同步代码执行完)。
-
五、事件循环:执行栈→微任务→宏任务的循环
执行栈为空后,JS 引擎进入事件循环(Event Loop),核心流程如下:
-
处理微任务队列:
依次将微任务队列中的回调函数推入执行栈执行(遵循 "后进先出",但队列本身是 "先进先出")。
- 若执行微任务过程中产生新的微任务(如
then
回调中又调用Promise.resolve().then()
),新微任务会被加入当前队列末尾,同批次执行(直到微任务队列为空)。
- 若执行微任务过程中产生新的微任务(如
-
(浏览器特有)UI 渲染:
微任务队列清空后,浏览器会判断是否需要重新渲染(如 DOM 有更新),执行一次 UI 渲染(与
requestAnimationFrame
回调配合)。 -
处理宏任务队列: 从宏任务队列中取第一个回调函数推入执行栈执行(同步执行其内部代码,包括可能的函数调用、新的异步任务)。
-
循环往复:
该宏任务执行完→执行栈为空→处理微任务队列→(可选)UI 渲染→取下一个宏任务→... 直到所有任务执行完毕。
六、特殊场景补充
1. 闭包与执行上下文的关系
当父函数执行完毕并出栈后,若子函数仍被外部引用(如作为返回值),子函数的作用域链会保留对父函数作用域的引用(即闭包)。此时父执行上下文虽已出栈,但子函数仍能访问父函数的变量(变量内存未被 GC 回收,因闭包持有引用)。只有当子函数也执行完毕且无引用时,父变量才会被回收。
2. 异步回调的执行条件
所有异步回调(微任务 / 宏任务)必须等待执行栈为空时才会入栈执行。即使宏任务队列中有回调,若微任务队列未清空,也不会执行宏任务(微任务优先级更高)。
七、总结:事件循环完整流程
- 初始化:创建执行栈,全局执行上下文入栈,经历创建阶段(变量提升等)和执行阶段(同步代码执行)。
- 函数调用:函数执行上下文入栈,执行内部同步代码,嵌套函数会入栈在栈顶,执行完后出栈。
- 同步执行完毕:执行栈为空,所有同步代码处理完成。
- 处理微任务:将微任务队列所有回调入栈执行(包括新增微任务),直到队列为空。
- (浏览器)UI 渲染:若有 DOM 更新,执行一次渲染。
- 处理宏任务:从宏任务队列取第一个回调入栈执行,执行完后重复步骤 3-5,进入下一次事件循环。
这一过程保证了 JS 在单线程下既能处理同步逻辑,又能通过异步任务和事件循环实现非阻塞执行,是理解 "异步代码为何按特定顺序执行" 的核心。
3:全局执行上下文
执行上下文(Execution Context) 是 JavaScript 代码执行时的 "环境容器",负责管理当前代码的变量、函数、作用域及 this 指向。全局执行上下文是最基础的执行上下文,在脚本加载时由 JS 引擎自动创建,贯穿整个程序生命周期(直到页面关闭或程序结束)。
每个执行上下文(包括全局和函数执行上下文)都包含三个核心属性: 变量对象(Variable Object,VO) / 活动对象(Activation Object,AO) 作用域链(Scope Chain) this 绑定
一、变量对象(VO)与活动对象(AO):数据存储的载体
变量对象是执行上下文中存储变量、函数声明的 "数据仓库",其形态会随执行阶段和上下文类型(全局 / 函数)变化。
1. 全局执行上下文的变量对象:全局对象(Global Object)
全局执行上下文的变量对象就是全局对象 (在浏览器中是window
,Node.js 中是global
),它是一个预定义的内置对象,具备以下特性:
- 全局对象是所有全局变量、全局函数的 "宿主":顶层声明的变量(如
var a = 1
)和函数(如function foo() {}
)会自动成为全局对象的属性。 - 全局对象包含 JS 预定义的属性和方法:如
parseInt
、Math
、JSON
等,可直接访问(例如window.parseInt
与parseInt
等价)。 - 顶层代码中,
this
指向全局对象(非严格模式下):例如console.log(this === window)
在浏览器中会输出true
。
注意:全局对象不可通过代码显式创建,且其属性在全局作用域中可直接访问(无需前缀)。
2. 函数执行上下文的活动对象(AO)
函数执行上下文的变量对象在进入执行阶段后被称为活动对象(AO),它与 VO 本质是同一个对象,区别在于:
- VO 是 "理论上的存储结构"(引擎内部实现,不可直接访问);
- AO 是 "被激活的 VO":当函数开始执行时,VO 被激活为 AO,此时其属性(变量、函数、arguments)可被访问。
活动对象的创建与初始化遵循以下规则:
- 创建时机:函数被调用时,在执行上下文进入 "创建阶段" 时生成。
- 初始化内容:
-
-
首先添加
arguments
对象(仅函数上下文有):存储函数的实参列表,包含length
属性(实参数量)和callee
属性(指向当前函数)。 -
其次添加函数内部的函数声明:将函数名作为 AO 的属性,值为函数体(优先于变量声明,即 "函数声明提升")。
-
最后添加函数内部的变量声明:将变量名作为 AO 的属性,初始值为
undefined
(即 "变量声明提升",但赋值操作留在执行阶段)。
-
总结:VO 与 AO 的关系
-
全局执行上下文 :变量对象 = 全局对象(如
window
),始终存在且可访问。 -
函数执行上下文:进入执行阶段前,变量对象(VO)是引擎内部的 "待激活状态";进入执行阶段后,VO 被激活为活动对象(AO),并可通过作用域链访问。
二、作用域链(Scope Chain):变量查找的路径
作用域链是执行上下文中的 "变量查找链条",本质是一个由当前上下文及所有父级上下文的变量对象(或 AO)组成的有序列表,用于确定变量的访问权限。
1. 作用域链的形成过程
作用域链的构建分为两个关键阶段:函数创建时和函数激活时。
-
函数创建阶段 : 函数在定义时(而非调用时)会记录父级作用域的变量对象链,存储在内部属性
[[Scope]]
中([[Scope]]
是引擎内部属性,不可通过代码访问)。例如:
jsvar globalVar = "全局变量"; function parent() { var parentVar = "父函数变量"; function child() { var childVar = "子函数变量"; } }
parent
函数创建时,[[Scope]]
=[全局执行上下文的VO(即window)]
;child
函数创建时,[[Scope]]
=[parent函数的AO, 全局执行上下文的VO]
。
-
函数激活阶段(执行时):
当函数被调用,进入执行上下文时,作用域链会以
[[Scope]]
为基础,将当前函数的 AO 添加到链的最前端(保证当前作用域的变量优先被查找)。 即:作用域链 = [当前函数AO].concat([[Scope]])
。
2. 变量查找规则
当访问一个变量时,JS 引擎会从作用域链的第一个对象(当前 AO) 开始查找:
-
若找到变量,直接使用;
-
若未找到,依次查找链中的下一个变量对象(父级 VO/AO);
-
直到找到全局对象仍未找到,则抛出
ReferenceError
。
三、this:上下文的 "所有者" 指针
this
是执行上下文中的一个特殊属性,指向当前代码的 "调用者",其值在执行上下文创建阶段(而非定义阶段) 确定,具体指向取决于调用方式。
1. 全局执行上下文中的 this
-
非严格模式 :
this
指向全局对象(浏览器中为window
,Node.js 中为global
)。 -
严格模式 :
this
为undefined
(避免全局变量污染)。
2. 函数执行上下文中的 this
函数中的this
指向由调用方式决定,常见场景包括:
-
普通函数调用 (如
foo()
):- 非严格模式 :
this
指向全局对象; - 严格模式 :
this
为undefined
。
- 非严格模式 :
-
对象方法调用 (如
obj.foo()
):this
指向调用方法的对象(obj
)。 -
构造函数调用 (如
new Foo()
):this
指向新创建的实例对象。 -
通过
apply/call/bind
调用 :this
指向传入的第一个参数(若为非对象,会被自动转为对象类型,除非为null/undefined
)。
四、执行上下文的生命周期与执行栈(ECStack)
执行上下文的生命周期分为创建阶段和执行阶段,由执行上下文栈(Execution Context Stack,ECStack)管理执行顺序。
1. 生命周期阶段
-
创建阶段(进入执行上下文):
完成三件事:
- 确定变量对象(VO)/ 活动对象(AO)(初始化变量、函数声明);
- 构建作用域链(结合
[[Scope]]
和 AO); - 确定
this
的指向。
-
执行阶段:
执行代码,完成变量赋值、函数调用等操作(即 "逐行执行,为变量赋真实值")。
2. 执行上下文栈(ECStack)的管理规则
执行上下文栈是引擎用于管理执行上下文的 "栈结构",遵循后进先出(LIFO) 原则:
-
脚本加载时,全局执行上下文首先入栈,且始终在栈底(直到程序结束才出栈)。
-
函数被调用时,其执行上下文入栈,成为 "当前执行上下文"。
-
函数执行完毕(或返回)时,其执行上下文出栈,栈顶指向之前的上下文(父级或全局)。
五、示例:全局与函数执行上下文的完整流程
以如下代码为例,拆解执行过程:
js
var globalVar = "全局变量"; // 全局变量
function checkScope() {
var scopeVar = "函数内变量"; // 函数内变量
return scopeVar;
}
checkScope(); // 调用函数
步骤 1:全局执行上下文创建与执行
-
创建阶段:
- 变量对象(VO) = 全局对象(
window
); - 初始化 VO :
globalVar = undefined
,checkScope
= 函数体(函数声明提升); - 作用域链 =
[window]
; this
=window
(非严格模式)。
- 变量对象(VO) = 全局对象(
-
执行阶段:
-
为
globalVar
赋值:globalVar = "全局变量"
; -
执行
checkScope()
,触发函数执行上下文的创建。
-
步骤 2:checkScope 函数执行上下文入栈
- 创建阶段 :
- 生成活动对象(AO):
arguments = { length: 0 }
,scopeVar = undefined
(变量声明提升); - 构建作用域链:
checkScope.[[Scope]]
=[window]
→ 作用域链 =[AO, window]
; this
=window
(非严格模式,普通函数调用)。
- 生成活动对象(AO):
- 执行阶段 :
- 为
scopeVar
赋值:scopeVar = "函数内变量"
; - 执行
return scopeVar
:通过作用域链在当前 AO 中找到scopeVar
,返回其值。
- 为
步骤 3:函数执行上下文出栈
checkScope
执行完毕,其执行上下文从 ECStack 中出栈,栈顶回到全局执行上下文。
六、补充说明
- 与事件循环的关联:全局执行上下文始终在 ECStack 中,直到所有代码(包括同步代码和异步任务回调)执行完毕才出栈,是事件循环中 "同步代码优先执行" 的基础。
- 闭包对作用域链的影响:若内部函数被外部引用(形成闭包),其作用域链会保留父级 AO(即使父函数已执行完毕),因此闭包可访问父函数的变量。
- ES6 的变化:ES6 中 "变量对象" 的概念被 "词法环境(Lexical Environment)" 和 "变量环境(Variable Environment)" 取代,但核心逻辑(变量存储、作用域管理)一致,可理解为 "更规范的 VO 实现"。
4:闭包补充说明
闭包是指那些能够访问,自由变量 的函数 自由变量 是指在函数 中使用的,但既不是函数参数也不是函数的局部变量的变量 组成 :闭包 = 函数 + 函数能够访问的自由变量
理论角度的闭包: 所有函数都是闭包。因 为函数在创建时会捕获上层作用域的变量对象(即保存[[Scope]]
),即使是最简单的全局函数,也能访问全局变量(自由变量),符合 "函数 + 自由变量" 的组成。
实践角度的闭包: 创建它的上下文已销毁 (如外部函数执行完毕并出栈); 仍能访问创建它时的自由变量 (即引用外部作用域的变量)。 核心 :闭包的本质是内部函数的作用域链 保留了对外部函数活动对象(AO)的引用,即使外部函数的执行上下文已从栈中移除,其 AO 因被引用而不会被垃圾回收(GC),从而使内部函数能持续访问这些变量。
一、闭包的核心定义与分类
1. 理论角度的闭包(ECMAScript 规范)
-
定义 :所有函数都是闭包。因为函数在创建时会捕获上层作用域的变量对象(即保存
[[Scope]]
),即使是最简单的全局函数,也能访问全局变量(自由变量),符合 "函数 + 自由变量" 的组成。 -
例:
jsvar globalVar = 1; function foo() { console.log(globalVar); } // foo是闭包:函数+自由变量globalVar
这里foo
能访问全局变量globalVar
(非参数 / 局部变量),因此是理论上的闭包。
2. 实践角度的闭包(开发中常用场景)
-
定义:满足两个条件的函数:
- 创建它的上下文已销毁(如外部函数执行完毕并出栈);
- 仍能访问创建它时的自由变量(即引用外部作用域的变量)。
-
核心:闭包的本质是内部函数的作用域链保留了对外部函数活动对象(AO)的引用,即使外部函数的执行上下文已从栈中移除,其 AO 因被引用而不会被垃圾回收(GC),从而使内部函数能持续访问这些变量。
二、闭包的作用域链保留机制(结合执行流程)
以 "内部函数返回" 案例为例,细化作用域链的维护过程:
js
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
} // f引用了自由变量scope(来自checkscope的AO)
return f;
}
var foo = checkscope();
foo(); // 输出 "local scope"
执行流程与作用域链分析:
- checkscope 函数创建时 : 其内部属性
[[Scope]]
=[全局执行上下文的VO(window)]
(记录父级作用域)。 - checkscope 调用时 :
- 进入函数执行上下文,创建活动对象
AO
:{ scope: undefined, f: 函数体 }
(变量声明提升)。 - 作用域链 =
[checkscope的AO].concat([[Scope]])
→[checkscopeAO, window]
。 - 执行阶段:为
scope
赋值"local scope"
,f
函数创建(此时f
的[[Scope]]
=[checkscopeAO, window]
,记录当前作用域链)。
- 进入函数执行上下文,创建活动对象
- checkscope 执行完毕 :
- 其执行上下文从执行栈中弹出,但
f
的[[Scope]]
仍保留对checkscopeAO
的引用(关键!)。
- 其执行上下文从执行栈中弹出,但
- foo () 调用时(即 f 执行时) :
- 进入
f
的执行上下文,创建活动对象AO
(空,因f
无局部变量 / 参数)。 - 作用域链 =
[f的AO].concat(f.[[Scope]])
→[fAO, checkscopeAO, window]
。 - 查找
scope
:从fAO
未找到 → 查找checkscopeAO
→ 找到scope: "local scope"
,返回结果。
- 进入
核心结论 :即使checkscope
的执行上下文已销毁,但其AO
因被f
的[[Scope]]
引用而未被 GC 回收,这就是闭包能访问 "已销毁上下文变量" 的本质。
三、经典案例优化:for 循环与闭包
1. 问题案例(var 声明导致的闭包陷阱)
js
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
- 原因分析 :
-
var i
的作用域是全局(或函数级),整个循环共享同一个i
(最终变为 3)。 -
每个
data[i]
函数的[[Scope]]
=[全局VO]
(因i
是全局变量),调用时查找i
会直接访问全局的i=3
。
-
2. 解决方案 1:立即执行函数(IIFE)创建闭包
js
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) { // 形参i接收当前循环的i值
return function () {
console.log(i);
};
})(i); // 实参i传入当前循环值}
data[0](); // 0
data[1](); // 1
data[2](); // 2
- 原理 :
-
每次循环创建一个独立的 IIFE 执行上下文,其
AO
中保存当前i
(形参赋值)。 -
内部返回的函数的
[[Scope]]
=[IIFE的AO, 全局VO]
,调用时会从 IIFE 的 AO 中查找i
(而非全局),因此得到各自的循环值。
-
3. 解决方案 2:let 声明(块级作用域)
js
var data = [];
for (let i = 0; i < 3; i++) { // let创建块级作用域,每次循环的i是独立变量
data[i] = function () {
console.log(i);
};
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
- 原理 :
-
let
在 for 循环中会为每次迭代创建独立的块级作用域,每个i
是该作用域的局部变量。 -
内部函数的
[[Scope]]
会引用当前块级作用域的变量对象,因此调用时能访问各自的i
。
-
四、闭包的实践价值与注意事项
1. 常见用途
-
模块化:封装私有变量(避免全局污染),如:
jsfunction createCounter() { let count = 0; // 私有变量,外部无法直接访问 return { increment: () => count++, getCount: () => count }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 1(通过闭包访问私有变量)
-
延迟执行:如定时器回调访问外部变量(避免变量被修改)。
2. 注意事项
- 内存泄漏风险 :闭包会保留对外部
AO
的引用,若闭包长期存在(如被全局变量引用),AO
不会被 GC 回收,可能导致内存占用过高。- 解决 :不再需要时,手动解除引用(如
counter = null
)。
- 解决 :不再需要时,手动解除引用(如
五、总结
闭包 的核心是 "函数对自由变量的引用 + 作用域链的保留 ":理论上所有函数都是闭包(因都访问自由变量 ),实践中闭包特指 "创建它的上下文销毁后仍能访问自由变量的函数 "。其本质是内部函数的[[Scope]]
保留了对外部活动对象(AO)的引用 ,使变量即使脱离原上下文仍可访问。理解这一点,能更清晰地解释循环陷阱、私有变量等经典场景,也能更合理地使用闭包避免内存问题。
5:ES6新特性
新特性主要归为四大类:
解决原有语法上的一些不足: 比如let 和 const 的块级作用域
对原有语法进行增强: 比如解构、展开、参数默认值、模板字符串
全新的对象、全新的方法、全新的功能: 比如promise、proxy、object的assign、is
全新的数据类型和数据结构: 比如symbol、set、map
一、解决原有语法的不足
核心:弥补 ES5 及之前语法的缺陷(如变量提升、作用域不明确等)。
1. let
与 const
(块级作用域)
-
核心改进 :引入块级作用域(
{}
内的作用域),解决var
的三大问题:- 变量提升导致的 "声明前可访问"(
var a = 1; if (true) { var a = 2; }
会污染外部); - 无块级作用域导致的循环闭包问题(如
for (var i=0;...)
中i
全局污染); - 可重复声明(
var a=1; var a=2
不报错)。
- 变量提升导致的 "声明前可访问"(
-
暂时性死区(TDZ) :变量在声明前不可访问(即使在
typeof
中),避免意外使用未初始化的变量:jsconsole.log(typeof a); // 报错(a在TDZ中) let a = 1;
const
细节:声明的变量不可 "重新赋值",但引用类型(对象 / 数组)的内部属性可修改(因存储的是地址):
js
const arr = [1, 2];
arr.push(3); // 合法:arr变为 [1,2,3]
arr = [4,5]; // 报错:重新赋值
2. 其他语法修正
-
函数参数默认值的作用域:ES6 中参数默认值形成独立作用域,避免与函数体内变量冲突:
jsfunction fn(x = y, y = 1) {} // 报错:x的默认值引用了未声明的y(y在参数作用域中)
二、对原有语法的增强
核心:简化代码编写,提升语法灵活性(如解构、模板字符串等)。
1. 解构赋值
-
对象解构:支持嵌套解构、默认值、重命名:
jsconst user = { name: 'Alice', age: 20, addr: { city: 'Beijing' } }; const { name: username, age = 18, addr: { city } } = user; // username = 'Alice',age = 20,city = 'Beijing'
-
数组解构:支持 "跳过元素""剩余参数":
jsconst [a, , b, ...rest] = [1, 2, 3, 4, 5]; // a=1,b=3,rest=[4,5]
2. 函数参数增强
-
参数默认值 :直接在参数列表定义默认值,避免
arguments
或逻辑判断:jsfunction greet(name = 'Guest') { console.log(`Hello, ${name}`); } greet(); // "Hello, Guest"
-
剩余参数(
...rest
) :替代arguments
(类数组),直接获取数组形式的参数:jsfunction sum(...nums) { return nums.reduce((a, b) => a + b); } sum(1, 2, 3); // 6(nums是真正的数组,支持数组方法)
3. 模板字符串
-
多行文本 :直接换行,无需
\n
:jsconst html = ` <div> <p> 多行文本 </p> </div> `;
-
标签函数(Tagged Templates):自定义字符串处理逻辑(如过滤 HTML、国际化):
jsfunction safeHTML(strings, ...values) { // strings是模板字符串的静态部分,values是插值表达式结果 return strings.reduce((res, str, i) => res + str + (values[i] || ''), ''); } const userInput = ''; const safe = safeHTML`${userInput}`; // 可在此处过滤危险内容
4. 扩展运算符(...
)
-
数组扩展 :浅拷贝、合并、转换类数组(如
NodeList
):jsconst arr1 = [1, 2]; const arr2 = [...arr1, 3, 4]; // [1,2,3,4](合并) const nodes = document.querySelectorAll('div'); const nodeArr = [...nodes]; // 转换为数组
-
对象扩展:合并对象(后出现的属性覆盖前者)、复制属性:
jsconst obj1 = { a: 1 }; const obj2 = { ...obj1, b: 2, a: 3 }; // { a:3, b:2 }
三、全新的对象、方法与功能
核心:新增 API 和工具,提升开发效率(如异步处理、元编程等)。
1. Promise
(异步处理)
- 解决问题:替代嵌套回调("回调地狱"),通过链式调用实现异步流程控制。
- 状态与方法 :
- 状态 :
pending
(初始)→fulfilled
(成功)/rejected
(失败)(状态不可逆)。 - 常用方法 :
-
Promise.resolve(value)
/Promise.reject(error)
:快速创建已决议的 Promise; -
Promise.all([p1, p2])
:全部成功则返回结果数组,任一失败则返回第一个失败原因; -
Promise.race([p1, p2])
:返回第一个完成(成功 / 失败)的结果; -
finally()
:无论成功 / 失败都会执行(如清理资源)。
-
- 状态 :
2. Proxy
与 Reflect
(元编程)
-
Proxy
:拦截对象的底层操作(如属性读写、函数调用),实现数据劫持、权限控制等:jsconst user = { name: 'Alice' }; const proxy = new Proxy(user, { get(target, prop) { // 拦截属性读取 console.log(`读取了${prop}`); return target[prop]; }, set(target, prop, value) { // 拦截属性赋值 if (prop === 'age' && value < 0) throw new Error('年龄无效'); target[prop] = value; } }); proxy.name; // 打印"读取了name",返回"Alice" proxy.age = -1; // 报错
-
Reflect
:与Proxy
配套的 API,提供对象操作的默认行为(如Reflect.get(target, prop)
等价于target[prop]
),便于在Proxy
中复用默认逻辑:jsconst proxy = new Proxy(user, { get(target, prop) { // 先执行自定义逻辑,再调用默认行为 return Reflect.get(target, prop); } });
3. 对象的新增方法
-
Object.assign(target, ...sources)
:合并对象(浅拷贝,仅复制自身可枚举属性); -
Object.is(a, b)
:更精准的相等判断(解决===
的缺陷,如Object.is(NaN, NaN) // true
,===
中NaN !== NaN
); -
Object.keys(obj)
/Object.values(obj)
/Object.entries(obj)
:获取对象的键、值、键值对数组(便于遍历); -
Object.getOwnPropertyDescriptors(obj)
:获取对象所有属性的描述符(用于复制 getter/setter)。
4. 字符串与数组的新增方法
- 字符串 :
includes(str)
:判断是否包含子串(替代indexOf
);startsWith(str)
/endsWith(str)
:判断是否以子串开头 / 结尾;padStart(length, padStr)
/padEnd(length, padStr)
:补全字符串长度(如日期格式化:'5'.padStart(2, '0') → '05'
)。
- 数组 :
-
Array.from(iterable)
:将类数组(如arguments
)或可迭代对象(如Set
)转为数组; -
Array.of(1, 2)
:创建数组(修复new Array(2)
创建立长度为 2 的空数组的问题); -
find(fn)
/findIndex(fn)
:查找第一个符合条件的元素 / 索引; -
includes(value)
:判断数组是否包含值(支持NaN
)。
-
四、全新的数据类型和数据结构
核心:引入新的类型和结构,满足复杂场景需求(如唯一性标识、键值对存储等)。
1. Symbol
(唯一标识符)
-
特性:创建后的值唯一,即使描述相同也不相等,用于避免属性名冲突:
jsconst s1 = Symbol('id'); const s2 = Symbol('id'); s1 === s2; // false(唯一)
-
应用:
-
作为对象的私有属性键(无法通过
for...in
或Object.keys
枚举); -
定义常量(如枚举值):
const STATUS = { PENDING: Symbol('pending'), DONE: Symbol('done') }
; -
为对象添加内置方法(如
Symbol.iterator
定义迭代器)。
-
2. Set
与 WeakSet
(集合)
-
Set
:无重复值的集合,支持增删查:jsconst s = new Set([1, 2, 2, 3]); s.size; // 3(自动去重) s.add(4); // 添加元素 s.has(2); // true(判断是否存在) s.delete(3); // 删除元素
-
WeakSet
:仅存储对象,且对对象的引用是 "弱引用"(不影响 GC 回收),适合临时存储对象(如 DOM 节点),无size
属性,不可遍历。
3. Map
与 WeakMap
(键值对集合)
-
Map
:键可以是任意类型(对象也可作为键),有序(按插入顺序):jsconst m = new Map(); const objKey = { id: 1 }; m.set(objKey, 'value'); // 以对象为键 m.get(objKey); // 'value'(通过对象获取值)
-
WeakMap
:键必须是对象,且为弱引用(键对象被回收后,对应键值对自动删除),适合存储对象的附加信息(如 DOM 节点的元数据),不可遍历。
五、其他重要特性补充
1. 类与继承(class
与 extends
)
-
语法糖:简化 ES5 的原型链继承,提供更清晰的类定义:
jsclass Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise`); } } // 继承 class Dog extends Animal { // 调用父类构造函数 constructor(name) { super(name); } // 重写父类方法 speak() { console.log(`${this.name} barks`); } } new Dog('Buddy').speak(); // "Buddy barks"
2. 模块化(ES Module
)
-
解决全局变量污染,实现代码复用和按需加载:
-
导出 :
export
(命名导出)、export default
(默认导出):js// module.jsexport const name = 'foo'; // 命名导出 export default function() { console.log('default'); } // 默认导出
-
导入 :
import
引入模块,支持重命名、导入全部:js// main.js import { name as moduleName } from './module.js'; // 命名导入(重命名) import defaultFn from './module.js'; // 默认导入 import * as module from './module.js'; // 导入全部(作为对象)
-
3. Generator
与 async/await
(异步流程控制)
-
Generator
:函数通过yield
暂停执行,next()
恢复,返回迭代器,可用于分步处理:jsfunction* gen() { yield 1; yield 2; return 3; } const g = gen(); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: 3, done: true }
-
async/await
:基于Promise
的语法糖,让异步代码像同步一样直观:jsasync function fetchData() { try { const res = await fetch('api/data'); // 等待Promise完成 const data = await res.json(); return data; } catch (err) { console.error('请求失败', err); } }
4. 迭代器与 for...of
- 迭代器:对象实现
Symbol.iterator
方法后可被迭代(返回{ next() { return { value, done } } }
),如数组、Set
、Map
等默认可迭代。 for...of
:遍历可迭代对象的值(替代for
循环和forEach
):
js
for (const num of [1, 2, 3]) {
console.log(num); // 1, 2, 3(遍历值)
}
六、总结
ES6 的新特性从语法、API、数据结构等多维度提升了 JavaScript 的表现力:
- 解决了
var
作用域、回调地狱等历史问题; - 增强了语法灵活性(解构、扩展运算符等);
- 引入了
Promise
、Proxy
等强大工具; - 新增了
Symbol
、Set
、Map
等数据类型,满足复杂场景需求。
这些特性是现代前端开发的基础,也是理解框架(如 Vue、React)内部实现的关键。
6:AMD和ESModule和CommonJS各个区别
维度 | AMD | ESModule | CommonJS |
---|---|---|---|
语法风格 | 函数式(define /require ) |
声明式(import /export ) |
动态(require /exports ) |
依赖处理 | 动态(运行时确定) | 静态(编译时确定) | 动态(运行时确定) |
加载方式 | 异步(回调) | 预加载(并行) | 同步(阻塞) |
顶层 this | 指向全局对象(如window ) |
指向undefined |
指向module.exports |
循环依赖 | 支持(回调中处理) | 支持(需谨慎未初始化值) | 部分支持(返回已初始化部分) |
Tree Shaking | 不支持 | 支持 | 不支持 |
缓存机制 | 手动管理(模块可重复加载) | 自动缓存(同一模块仅加载一次) | 自动缓存(首次加载后缓存) |
文件扩展名 | .js |
.js (需type="module" )或.mjs |
.js (Node.js 默认) |
适用环境 | 浏览器(RequireJS) | 浏览器和 Node.js | Node.js |
应用场景差异
1. AMD 的典型场景
-
浏览器环境中需异步加载模块(如大型前端应用)。
-
兼容性要求高(支持旧版浏览器)。
2. ESModule 的典型场景
-
现代前端项目(配合 Webpack/Rollup 打包)。
-
浏览器原生模块(直接在 HTML 中使用)。
-
Node.js(从 v13.2 起原生支持)。
3. CommonJS 的典型场景
-
Node.js 服务器端开发(如 Express/Koa 框架)。
-
无需打包的脚本工具(如 CLI 工具)。
7:深拷贝和浅拷贝的区别
深拷贝:是指完全复制一个对象(包括嵌套对象),新对象与原对象完全独立,修改新对象不会影响原对象。 实现深拷贝的核心是递归遍历对象的所有属性,对基本类型直接复制,对引用类型(对象、数组等)则递归复制其内部属性。
浅拷贝:只复制顶层属性,嵌套对象仍共享引用(如 Object.assign()、扩展运算符 ...)。
一、简单场景实现方式
-
JSON.parse(JSON.stringify())
:优点:简单快捷。缺点:无法处理Function
、RegExp
、Date
(会转为字符串)、循环引用(直接报错),适合纯 JSON 数据场景。jsconst cloneObj = JSON.parse(JSON.stringify(originalObj));
二、完善版本实现方式
-
Date
:通过new Date()
复制时间戳。 -
RegExp
:复制源文本(source
)和修饰符(flags
)。 -
Reflect.ownKeys(target)
:获取所有自身键(包括Symbol
键),比for...in
更全面。 -
Map
:缓存克隆体,避免循环引用,如obj.self = obj
jsfunction deepClone(target, map = new Map()) { // 基本类型直接返回 if (target === null || typeof target !== 'object') { return target; } // 若已复制过该对象,直接返回缓存的克隆体(解决循环引用) if (map.has(target)) { return map.get(target); } let clone; // 处理日期 if (target instanceof Date) { clone = new Date(); clone.setTime(target.getTime()); return clone; } // 处理正则 if (target instanceof RegExp) { clone = new RegExp(target.source, target.flags); return clone; } // 初始化克隆体并缓存 clone = Array.isArray(target) ? [] : {}; map.set(target, clone); // 缓存映射关系 // 递归复制属性 Reflect.ownKeys(target).forEach(key => { clone[key] = deepClone(target[key], map); // 传递 map 缓存 }); return clone;
8:定时器
一、定时器基础:setTimeout
与 setInterval
是什么?
setTimeout
和 setInterval
是浏览器提供的 定时器 API,用于延迟或重复执行代码,属于 宏任务(由宿主环境发起,回调函数进入宏任务队列)。
1. setTimeout
:延迟执行一次
-
作用:指定延迟时间后,执行一次回调函数。
-
语法:
jsconst timerId = setTimeout(callback, delay[, arg1, arg2, ...]);
callback
:延迟后执行的函数(必选)。delay
:延迟时间(毫秒,默认 0),表示 "最早执行时间"(非 "精确执行时间",见后文)。arg1...
:传递给callback
的参数(可选)。- 返回值
timerId
:定时器唯一标识,用于取消定时器。
-
示例:
js// 1秒后打印消息 const timer = setTimeout((name) => { console.log(`Hello, ${name}!`); }, 1000, '前端'); // 传递参数 '前端' console.log('开始等待...'); // 先执行
2. setInterval
:间隔重复执行
-
作用:每隔指定时间,重复执行回调函数。
-
语法:
jsconst timerId = setInterval(callback, interval[, arg1, arg2, ...]);
interval
:间隔时间(毫秒),表示两次执行的 "最小间隔"。- 其他参数同
setTimeout
。
-
示例:
js// 每隔1秒打印当前时间let count = 0; const timer = setInterval(() => { count++; console.log(`第${count}次执行:${new Date().toLocaleTimeString()}`); if (count >= 3) clearInterval(timer); // 执行3次后停止 }, 1000);
3. 取消定时器:clearTimeout
与 clearInterval
-
作用 :通过
timerId
取消未执行的定时器(避免内存泄漏)。 -
语法:
jsclearTimeout(timerId); // 取消 setTimeout clearInterval(timerId); // 取消 setInterval
-
示例:
jsconst timer = setTimeout(() => { console.log('这行不会执行'); }, 1000); clearTimeout(timer); // 立即取消
二、核心原理:定时器为什么 "不准时"?
定时器的延迟 / 间隔时间 不是精确的,因为受 JS 单线程和事件循环 影响:
-
回调进入宏任务队列:
setTimeout
/setInterval
的回调函数会被放入 宏任务队列,需等待当前执行栈清空(同步代码、微任务执行完毕)后才会执行。 -
延迟是 "最早执行时间":
-
- 若
delay=1000
,表示 "至少 1 秒后执行",但如果主线程被阻塞(如长任务执行),实际执行时间会更长。 - 例:
js// 主线程被阻塞3秒 console.log('开始'); setTimeout(() => console.log('1秒后执行?'), 1000); // 模拟长任务(阻塞主线程) const start = Date.now();while (Date.now() - start < 3000) {} // 3秒后才结束 // 实际:3秒后才打印"1秒后执行?"(因主线程被占用)
- 若
三、setInterval
的 "累积执行" 问题
setInterval
存在一个隐患:若回调函数执行时间 超过间隔时间,会导致回调 "累积执行"(前一次未完成,下一次已入队)。
-
示例:
js// 间隔1秒执行,但回调需要2秒 setInterval(() => { console.log('开始执行'); // 模拟耗时2秒的任务 const start = Date.now(); while (Date.now() - start < 2000) {} console.log('执行结束'); }, 1000);
- 结果:回调执行时间(2 秒)> 间隔(1 秒),导致下一次回调在本次结束前已入队,最终 "连续执行",无间隔。
-
解决 :用
setTimeout
嵌套实现 "固定间隔"(确保前一次执行完再安排下一次):jsfunction repeatTask() { console.log('执行任务'); // 任务完成后,再延迟1秒安排下一次 setTimeout(repeatTask, 1000); } repeatTask(); // 首次执行
四、常见误区与注意事项
1. delay=0
不代表 "立即执行"
-
setTimeout(callback, 0)
的回调会进入宏任务队列,需等待:- 当前同步代码执行完毕;
- 所有微任务执行完毕。
-
示例:
jsconsole.log('同步代码1'); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve().then(() => console.log('微任务')); console.log('同步代码2'); // 执行顺序:同步代码1 → 同步代码2 → 微任务 → setTimeout
2. 回调函数中的 this
指向
-
非严格模式 :回调函数中
this
指向全局对象(window
); -
严格模式 :
this
为undefined
。 -
解决 :用箭头函数(继承外部
this
)或bind
绑定:jsconst obj = { name: '前端', start() { // 箭头函数继承 obj 的 this setTimeout(() => { console.log(this.name); // 输出 '前端' }, 1000); } }; obj.start();
3. 定时器的 "最小延迟" 限制
-
浏览器对
delay
有最小限制(通常 4ms),尤其是嵌套层级超过 5 层的定时器(为避免恶意代码占用资源)。 -
例:多层嵌套的
setTimeout
延迟会被强制改为 4ms:jslet delay = 0; let count = 0; function log() { console.log(delay); count++; if (count < 10) { setTimeout(log, delay); delay = Math.max(delay + 1, 0); } } log(); // 前几次可能为0,后续会固定为4ms
4. 页面隐藏时的定时器行为
- 当页面处于后台(如切换标签页),浏览器会降低定时器频率(如
setInterval
可能变为 1 次 / 秒),节省资源。
五、实践技巧:如何用好定时器?
-
避免长时间阻塞主线程:定时器回调应尽量轻量化, heavy 任务(如大数据处理)移至
Web Worker
。 -
及时取消定时器:组件销毁、页面卸载时,用
clearTimeout
/clearInterval
取消定时器,避免内存泄漏:js// 页面关闭前清理 window.addEventListener('beforeunload', () => { clearTimeout(timer); });
-
替代方案:
-
动画场景:用
requestAnimationFrame
(与渲染帧同步,更流畅); -
高精度计时:用
performance.now()
(比Date.now()
更精确)。
-
六、总结
setTimeout
用于 "延迟执行一次",setInterval
用于 "重复执行",均返回timerId
用于取消。- 定时器是宏任务,回调执行时间受主线程繁忙程度影响,非绝对精确。
- 注意
setInterval
的 "累积执行" 问题,优先用setTimeout
嵌套实现可控间隔。
掌握定时器的原理和细节,能避免 "计时不准""内存泄漏" 等问题,在异步场景(如延迟加载、轮询)中更高效地使用。
9:requestAnimationFrame 与 setTimeout、setInterval 的区别
一、requestAnimationFrame 核心知识点
requestAnimationFrame
是浏览器提供的 专门用于动画绘制的 API,其设计初衷是解决传统定时器(setTimeout/setInterval)在动画场景中可能出现的卡顿、掉帧问题,让动画更流畅。
-
基本语法
jsconst animationId = requestAnimationFrame(callback);
-
使用示例
js// 获取DOM元素 const box = document.getElementById('box'); let position = 0; // 初始位置 const speed = 2; // 每次移动的距离(像素) const maxWidth = window.innerWidth - 50; // 最大移动距离(减去方块宽度) // 动画回调函数 function animate(timestamp) { // 更新位置 position += speed; // 边界判断:到达最右侧则停止动画 if (position >= maxWidth) { position = maxWidth; // 固定在边界 box.style.left = `${position}px`; return; // 停止动画 } // 更新元素位置 box.style.left = `${position}px`; // 继续请求下一次动画(形成循环) requestAnimationFrame(animate); } // 启动动画 const animationId = requestAnimationFrame(animate); // 如需中途停止动画,可调用: // cancelAnimationFrame(animationId);
callback
:回调函数,接收一个参数timestamp
(当前时间戳,单位毫秒),用于计算动画进度。- 返回值
animationId
:一个整数标识,可通过cancelAnimationFrame(animationId)
取消动画。
-
核心特性
-
执行时机:由浏览器 在下一次重绘前调用,执行频率与浏览器刷新率保持一致(通常为 60Hz,即约 16.7ms 一次)。
-
自动适配:若页面处于后台或隐藏状态,浏览器会暂停执行,节省性能;当页面恢复显示时自动恢复。
-
宏任务归属:属于 宏任务,但优先级高于 setTimeout/setInterval(浏览器会优先处理动画相关任务)。
-
单次执行 :仅执行一次回调,若需连续动画,需在回调中再次调用
requestAnimationFrame
。
-
二、requestAnimationFrame 与 setTimeout、setInterval 的区别
三者虽都可用于延迟或重复执行代码,但设计目标和行为差异显著,具体区别如下:
对比维度 | setTimeout | setInterval | requestAnimationFrame |
---|---|---|---|
设计目标 | 延迟执行一次代码 | 每隔指定时间重复执行代码 | 专门用于动画绘制,保证流畅性 |
执行频率 | 由用户指定延迟时间(如 100ms) | 由用户指定间隔时间(如 100ms) | 与浏览器刷新率同步(约 16.7ms) |
时间精度 | 低,受宏任务队列阻塞影响 | 低,可能出现累积延迟(前一次未执行完,后一次又触发) | 高,由浏览器控制,与重绘同步 |
后台行为 | 即使页面隐藏,仍会按时执行 | 页面隐藏时仍可能执行 | 页面隐藏时自动暂停,节省性能 |
动画场景适配 | 可能卡顿(时间不匹配刷新率) | 易掉帧(间隔固定,不贴合重绘) | 完美适配,动画流畅无卡顿 |
使用场景 | 单次延迟任务(如弹窗延迟显示) | 周期性任务(如轮播图切换) | 动画绘制(如 DOM 动画、Canvas 动画) |
取消方式 | clearTimeout(id) | clearInterval(id) | cancelAnimationFrame(id) |
三、总结
- setTimeout/setInterval:适合一般的延迟或周期性任务(如定时请求数据、轮播图),但时间精度低,动画场景易卡顿。
- requestAnimationFrame:专为动画设计,性能更优、流畅度更高,是前端动画的首选方案(如页面滚动动画、元素位移 / 缩放等)。
10:call、apply、bind 的核心作用
三者都是 JavaScript 中改变函数执行时
this
指向的方法,适用于需要灵活指定函数上下文(即this
的指向)的场景(如借用其他对象的方法、实现继承等)。
一、call、apply、bind 的核心作用
三者都是 JavaScript 中改变函数执行时 this
指向的方法,适用于需要灵活指定函数上下文(即 this
的指向)的场景(如借用其他对象的方法、实现继承等)。
二、基本语法与使用示例
1. call
方法
-
作用 :立即调用函数,同时指定
this
指向,并传入参数列表(逐个传入)。 -
语法:
jsfunction.call(thisArg, arg1, arg2, ...);
thisArg
:函数执行时this
的指向(若为null/undefined
,非严格模式下this
指向全局对象,严格模式下为undefined
)。arg1, arg2...
:传递给函数的参数(可选)。
-
示例:
jsconst obj = { name: 'Alice' }; function greet(greeting, punctuation) { console.log(`${greeting}, ${this.name}${punctuation}`); } // 用 call 改变 this 为 obj,同时传参 greet.call(obj, 'Hello', '!'); // 输出:"Hello, Alice!"
2. apply
方法
-
作用 :立即调用函数,同时指定
this
指向,并传入参数数组(或类数组对象)。 -
语法:
jsfunction.apply(thisArg, [argsArray]);
thisArg
:同call
,指定this
指向。argsArray
:参数数组(如[arg1, arg2]
),若无需传参,可传null
或空数组。
-
示例:
js// 复用上面的 greet 函数和 obj 对象 greet.apply(obj, ['Hi', '~']); // 输出:"Hi, Alice~" // 实际应用:求数组最大值(借助 Math.max,它不支持直接传数组) const numbers = [1, 3, 2]; const max = Math.max.apply(null, numbers); // 等价于 Math.max(1,3,2),输出 3
3. bind
方法
-
作用 :不立即调用函数,而是返回一个新函数,新函数的
this
被永久绑定到thisArg
,且可预先传入部分参数(柯里化)。 -
语法:
jsconst newFunction = function.bind(thisArg, arg1, arg2, ...);
thisArg
:新函数执行时this
的指向(一旦绑定,无法再次通过call/apply/bind
修改)。arg1, arg2...
:预先传入的参数(后续调用新函数时,会在这些参数后追加新参数)。
-
示例:
js// 复用上面的 greet 函数和 obj 对象 const boundGreet = greet.bind(obj, 'Hello'); // 绑定 this 为 obj,预先传入第一个参数 'Hello' // 调用新函数时,只需传剩余参数 boundGreet('!'); // 输出:"Hello, Alice!"(等价于 greet.call(obj, 'Hello', '!'))
三、三者的核心区别
对比维度 | call |
apply |
bind |
---|---|---|---|
执行时机 | 立即执行函数 | 立即执行函数 | 返回新函数,需手动调用 |
参数传递方式 | 逐个传入参数(arg1, arg2...) | 传入参数数组([arg1, arg2...]) | 可预先传入部分参数(柯里化) |
this 绑定 |
临时绑定,仅本次有效 | 临时绑定,仅本次有效 | 永久绑定,新函数的 this 固定 |
返回值 | 函数执行的返回值 | 函数执行的返回值 | 新的绑定函数 |
四、典型应用场景
1. 改变 this
指向,借用其他对象的方法
js
// 场景:用数组的方法处理类数组对象(如 arguments)
function sum() {
// 用 Array.prototype.reduce 处理 arguments(类数组)
return Array.prototype.reduce.call(arguments, (total, num) => total + num, 0);
}
sum(1, 2, 3); // 6(arguments 借助数组的 reduce 方法求和)
2. 实现类的继承(借用父类构造函数)
js
function Parent(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
function Child(name, age) {
// 用 call 调用 Parent,将 this 指向 Child 实例,实现属性继承
Parent.call(this, name);
this.age = age;
}
const child = new Child('Alice', 18);
child.sayName();
// "Alice"(继承了 Parent 的 sayName 方法)
3. 柯里化(固定部分参数,复用函数)
js
// 场景:固定一个参数,生成新函数
function multiply(a, b) { return a * b; }
// 用 bind 固定第一个参数为 2,生成新函数(计算 2*b)
const multiplyBy2 = multiply.bind(null, 2); multiplyBy2(3);
// 6(等价于 multiply(2, 3))
multiplyBy2(4); // 8(复用性提升)
4. 解决回调函数中 this
丢失问题
js
const obj = {
value: 10,
getValue() {
// 回调函数中 this 可能指向全局,用 bind 绑定到 obj
setTimeout(function() {
console.log(this.value);
}.bind(this), 1000); // 此处 this 是 obj,绑定后回调中的 this 也指向 obj
}
};
obj.getValue();
// 10(1秒后输出,this 正确指向 obj)
五、注意事项
-
thisArg
为基本类型时的处理: 若thisArg
是字符串、数字、布尔值等基本类型,call/apply/bind
会自动将其转为对应的包装对象(如String
、Number
):jsfunction logThis() { console.log(this); } logThis.call(123); // Number { 123 }(基本类型 123 被转为包装对象)
-
bind
绑定的this
不可修改: 用bind
生成的新函数,其this
被永久绑定,后续再用call/apply
也无法修改:jsconst obj1 = { name: 'obj1' }; const obj2 = { name: 'obj2' }; function sayName() { console.log(this.name); } const boundFn = sayName.bind(obj1); boundFn.call(obj2); // "obj1"(this 仍指向 obj1,未被 obj2 修改)
-
箭头函数不适用: 箭头函数没有自己的
this
(继承外层上下文的this
),因此call/apply/bind
无法改变其this
指向(仅能传递参数):jsconst arrowFn = () => { console.log(this); }; arrowFn.call({ name: 'test' }); // window(非严格模式下,this 仍指向外层上下文)
六、总结
- 核心功能 :改变函数执行时的
this
指向,提升函数的灵活性和复用性。 - 选择依据 :
- 需立即执行 + 参数少:用
call
(参数逐个传); - 需立即执行 + 参数多(数组形式):用
apply
; - 需延迟执行 + 固定
this
或固定部分参数:用bind
。
- 需立即执行 + 参数少:用
- 理解三者的区别,能更精准地处理函数上下文,避免
this
指向混乱导致的 bug。
11:JS设计模式
单例模式(Singleton)
-
定义:确保一个类仅有一个实例,并提供全局访问点。
-
核心思想:限制实例化次数,避免重复创建消耗资源的对象(如全局缓存、模态框)。
-
示例
jsclass Singleton { constructor() { if (Singleton.instance) return Singleton.instance; // 已有实例直接返回 Singleton.instance = this; } static getInstance() { if (!Singleton.instance) new Singleton(); return Singleton.instance; } } const a = new Singleton(); const b = Singleton.getInstance(); console.log(a === b); // true(唯一实例)
-
应用场景:全局状态管理(如 Vuex 的 store)、弹窗组件、浏览器的
window
对象。
策略模式(Strategy)
-
定义:定义一系列算法,将其封装为独立策略,使算法可动态切换。
-
核心思想:分离算法的定义与使用,避免大量
if-else
或switch
判断。 -
示例:表单验证策略
js// 策略集合 const strategies = { isEmail: (value) => /^[\w-]+@([\w-]+\.)+[\w-]+$/.test(value), isMobile: (value) => /^1[3-9]\d{9}$/.test(value) }; // 验证器(使用策略) class Validator { constructor() { this.rules = []; } add(value, strategy) { this.rules.push(() => strategies[strategy](value)); } validate() { return this.rules.every(rule => rule()); } } // 使用 const validator = new Validator(); validator.add('test@example.com', 'isEmail'); console.log(validator.validate()); // true
-
应用场景:表单验证、支付方式选择(不同支付策略)、排序算法切换。
代理模式(Proxy)
-
定义:为对象提供一个代理层,控制对原对象的访问(如增强功能、限制权限)。
-
核心思想:不直接访问目标对象,通过代理间接交互,实现 "控制" 或 "增强"。
-
示例:图片懒加载代理
js// 目标对象:加载真实图片 class RealImage { constructor(url) { this.url = url; } load() { console.log(`加载图片: ${this.url}`); } } // 代理对象:先显示占位图,加载完成后再调用真实加载 class ImageProxy { constructor(url) { this.url = url; this.realImage = null; } load() { console.log('显示占位图...'); // 代理增强 this.realImage = this.realImage || new RealImage(this.url); this.realImage.load(); // 访问目标对象 } } // 使用 const img = new ImageProxy('https://example.com/img.jpg'); img.load(); // 输出:显示占位图... 加载图片: https://example.com/img.jpg
-
应用场景:图片懒加载、权限控制(代理拦截无权限操作)、缓存代理(如计算结果缓存)。
装饰者模式(Decorator)
-
定义:动态地给对象添加额外职责,不改变原对象结构。
-
核心思想:"包装" 原对象,扩展功能且不破坏原有逻辑(遵循开放 - 封闭原则)。
-
示例:给函数添加日志功能
js// 原函数 function fetchData() { console.log('获取数据...'); } // 装饰者:添加日志 function withLog(fn) { return function(...args) { console.log(`[日志] 调用函数: ${fn.name}`); // 新增职责 return fn.apply(this, args); // 执行原函数 }; } // 使用装饰后的函数 const fetchDataWithLog = withLog(fetchData); fetchDataWithLog(); // 输出:[日志] 调用函数: fetchData 获取数据...
-
应用场景:日志记录、性能监控(统计函数执行时间)、权限校验(装饰接口请求)。
组合模式(Composite)
-
定义:将对象组合成树形结构,使单个对象和组合对象具有一致的接口(统一操作)。
-
核心思想:"部分 - 整体" 关系,无论是单个元素还是容器,都可通过相同方法操作(如渲染、删除)。
-
示例:菜单组件(单个菜单项与子菜单)
js// 抽象组件(统一接口) class Component { render() { throw new Error('需实现render方法'); } } // 叶子节点(单个菜单项) class MenuItem extends Component { constructor(name) { this.name = name; } render() { console.log(`渲染菜单项: ${this.name}`); } } // 组合节点(子菜单,包含多个组件) class SubMenu extends Component { constructor(name) { super(); this.name = name; this.children = []; } add(child) { this.children.push(child); } render() { console.log(`渲染子菜单: ${this.name}`); this.children.forEach(child => child.render()); // 统一调用子组件 } } // 使用const menu = new SubMenu('文件'); menu.add(new MenuItem('新建')); menu.add(new MenuItem('保存')); menu.render(); // 输出:// 渲染子菜单: 文件// 渲染菜单项: 新建// 渲染菜单项: 保存
-
应用场景:UI 组件树(如 React/Vue 组件)、文件夹结构、权限菜单。
工厂模式(Factory)
-
定义:提供创建对象的接口,隐藏具体实例化逻辑(根据参数返回不同类型对象)。
-
核心思想:解耦对象创建与使用,简化复杂对象的创建过程。
-
示例:简单工厂(创建不同类型的按钮)
jsclass Button { constructor(text) { this.text = text; } render() { throw new Error('需实现render方法'); } } class PrimaryButton extends Button { render() { console.log(`[主按钮] ${this.text}`); } } class SecondaryButton extends Button { render() { console.log(`[次要按钮] ${this.text}`); } } // 工厂类 class ButtonFactory { static create(type, text) { switch(type) { case 'primary': return new PrimaryButton(text); case 'secondary': return new SecondaryButton(text); default: throw new Error('未知按钮类型'); } } } // 使用const btn1 = ButtonFactory.create('primary', '提交'); const btn2 = ButtonFactory.create('secondary', '取消'); btn1.render(); // [主按钮] 提交 btn2.render(); // [次要按钮] 取消
-
应用场景:UI 库(如创建不同主题的组件)、数据解析(根据类型解析不同格式数据)。
访问者模式(Visitor)
-
定义:封装对对象结构中元素的操作,使操作与元素结构分离(可动态添加新操作)。
-
核心思想:针对复杂对象结构(如树形、集合),将 "操作" 抽离为独立的访问者,避免修改元素类。
-
示例:对不同形状应用 "计算面积" 和 "绘制" 操作
js// 元素类(形状) class Circle { constructor(radius) { this.radius = radius; } accept(visitor) { visitor.visitCircle(this); } // 接受访问者 } class Rectangle { constructor(width, height) { this.width = width; this.height = height; } accept(visitor) { visitor.visitRectangle(this); } } // 访问者(操作) class AreaVisitor { visitCircle(circle) { console.log(`圆形面积: ${Math.PI * circle.radius **2}`); } visitRectangle(rect) { console.log(`矩形面积: ${rect.width * rect.height}`); } } class DrawVisitor { visitCircle(circle) { console.log(`绘制圆形(半径: ${circle.radius})`); } visitRectangle(rect) { console.log(`绘制矩形(${rect.width}x${rect.height})`); } } // 使用 const shapes = [new Circle(5), new Rectangle(4, 6)]; const areaVisitor = new AreaVisitor(); const drawVisitor = new DrawVisitor(); shapes.forEach(shape => { shape.accept(areaVisitor); // 应用面积计算 shape.accept(drawVisitor); // 应用绘制操作 });
-
数据结构的多维度操作(如 AST 语法树的分析、转换)、报表生成(同一数据生成不同格式报表)。
观察者模式(Observer Pattern)
-
定义 :定义对象间的一对多依赖关系,当 "主题(Subject)" 状态变化时,所有 "观察者(Observer)" 自动收到通知并更新。
-
核心思想 :主题与观察者直接关联(紧耦合),主题主动通知观察者。
-
示例 :简单的订阅通知
js// 主题(被观察者) class Subject { constructor() { this.observers = []; } // 维护观察者列表 addObserver(observer) { this.observers.push(observer); } notify(data) { // 通知所有观察者 this.observers.forEach(observer => observer.update(data)); } } // 观察者 class Observer { constructor(name) { this.name = name; } update(data) { console.log(`${this.name} 收到通知: ${data}`); } } // 使用const subject = new Subject(); const observer1 = new Observer('用户1'); const observer2 = new Observer('用户2'); subject.addObserver(observer1); subject.addObserver(observer2); subject.notify('数据更新了!'); // 输出:// 用户1 收到通知: 数据更新了!// 用户2 收到通知: 数据更新了!
-
应用场景 :DOM 事件(
addEventListener
)、Vue 的响应式原理(数据变化触发视图更新)。
发布 - 订阅模式(Publish-Subscribe Pattern)
-
定义 :通过 "事件总线(Event Bus)" 实现解耦,发布者(Publisher)发布事件,订阅者(Subscriber)订阅事件,两者无直接关联。
-
核心思想:中间层(事件总线)转发事件,发布者和订阅者完全解耦。
-
示例:基于事件总线的通信
js// 事件总线(中间层) class EventBus { constructor() { this.events = {}; } // 键:事件名,值:回调数组 on(event, callback) { // 订阅事件 this.events[event] = this.events[event] || []; this.events[event].push(callback); } emit(event, data) { // 发布事件 if (!this.events[event]) return; this.events[event].forEach(cb => cb(data)); // 触发所有订阅者 } } // 使用 const bus = new EventBus(); // 订阅者1订阅'news'事件 bus.on('news', (data) => console.log('订阅者A收到新闻:', data)); // 订阅者2订阅'news'事件 bus.on('news', (data) => console.log('订阅者B收到新闻:', data)); // 发布者发布'news'事件 bus.emit('news', '今日有雨'); // 输出:// 订阅者A收到新闻: 今日有雨// 订阅者B收到新闻: 今日有雨
-
应用场景 :跨组件通信(如 Vue 的 EventBus)、前端事件系统(如 Node.js 的
EventEmitter
)、消息队列。
观察者模式 vs 发布 - 订阅模式的核心区别
** 维度 ** | ** 观察者模式 ** | ** 发布 - 订阅模式 ** |
---|---|---|
** 耦合度 ** | 紧耦合:主题直接持有观察者引用 | 松耦合:通过事件总线间接通信,发布者与订阅者互不感知 |
** 中间层 ** | 无,主题直接通知观察者 | 有(事件总线),负责转发事件 |
** 通信方式 ** | 主题主动调用观察者的更新方法 | 发布者触发事件,总线通知订阅者 |
** 适用场景 ** | 简单的一对一 / 一对多关系(如数据 - 视图绑定) | 复杂系统的解耦(如跨模块、跨页面通信) |
最后
OK 以上就是本期关于 JS 基础的笔记内容啦。JS 基础知识点多且杂,但只要理清逻辑、结合场景记忆,就能举一反三。这些内容不仅能帮你应对面试,更能优化日常开发中的代码质量。
这篇笔记前后改了 3 版,尽量把每个知识点的 "考点陷阱" 标出来了,如果对你有帮助,记得点赞收藏支持一下!大家觉得哪些模块需要展开细讲,评论区告诉我~ 反馈不错的话,后续会继续分享VUE框架内容,学前端的朋友别错过,咱们下期见。