面试被 JS 基础问懵?11 个核心模块笔记帮你吃透(含考点陷阱)

前端面试复习笔记: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()入栈后在栈顶,优先执行)。
    • 函数执行完毕后出栈,直到全局执行上下文出栈(此时调用栈为空)。
  • 作用:所有代码(同步代码、异步任务的回调)最终都需通过执行栈执行,是 JS "单线程" 特性的直接体现。

2. 宏任务(MacroTasks)
  • 定义:由宿主环境(浏览器 / Node.js)发起的异步任务,执行优先级较低,每次事件循环仅执行一个宏任务。
  • 常见类型
环境 宏任务类型
浏览器 script(整体代码,第一个宏任务)、setTimeoutsetIntervalI/O(如网络请求)、UI渲染MessageChannelrequestAnimationFrame(与渲染帧同步)
Node.js setTimeoutsetIntervalI/OsetImmediateclose事件(如文件关闭)
  • 特点:宏任务执行前,会先清空所有微任务(保证微任务优先于下一个宏任务执行)。
3. 微任务(MicroTasks)
  • 定义:由 JS 引擎自身发起的异步任务,执行优先级高于宏任务,每次宏任务执行后会清空所有微任务(包括执行过程中新增的微任务)。
  • 常见类型
环境 微任务类型
浏览器 Promise.then/catch/finallyMutationObserver(监听 DOM 变化)、queueMicrotask(显式添加微任务)
Node.js Promise.then/catch/finallyprocess.nextTick(优先级高于其他微任务)、queueMicrotask
  • 特点 :微任务执行时若产生新的微任务(如在then中再次调用queueMicrotask),会被加入当前微任务队列末尾,同批次执行(不会等待下一个宏任务)。
4. 事件循环(Event Loop)
  • 本质:JS 单线程下协调 "执行栈""宏任务队列""微任务队列" 的机制,保证异步任务有序执行。

  • 核心目标:在单线程限制下,通过 "循环取任务 - 执行" 的方式,实现非阻塞的异步操作(如网络请求、定时器)。

二、完整执行流程(以浏览器为例)
  1. 初始化:第一个宏任务(script整体代码)
    • 全局执行上下文入栈,开始执行script中的同步代码:
      • 同步代码直接在执行栈中执行,执行完后出栈。
      • 遇到函数调用:函数执行上下文入栈,执行完后出栈(遵循 LIFO)。
      • 遇到宏任务(如setTimeout):回调函数被添加到 "宏任务队列"(等待执行)。
      • 遇到微任务(如Promise.then):回调函数被添加到 "微任务队列"(等待执行)。
  2. 第一次宏任务执行完毕
    • script同步代码执行完,全局执行上下文出栈,执行栈为空。
  3. 清空微任务队列
    • 从微任务队列中依次取出所有回调,入栈执行(包括执行过程中新增的微任务,全部执行完才停止)。
    • 例:若then回调中调用queueMicrotask(() => { ... }),新微任务会被加入当前队列,同批次执行。
  4. 执行 UI 渲染(浏览器特有)
    • 微任务队列清空后,浏览器会判断是否需要渲染(如 DOM 有变化),执行一次 UI 渲染(与requestAnimationFrame回调配合)。
  5. 循环执行:取下一个宏任务
    • 从宏任务队列中按 "先进先出(FIFO)" 取第一个任务(如setTimeout回调),入栈执行。

    • 重复步骤 2-4:该宏任务的同步代码执行完→执行栈为空→清空微任务队列→(可选)UI 渲染→取下一个宏任务...

三、关键细节补充

1.宏任务与微任务的优先级差异

  • 微任务优先级 高于 宏任务:同一轮事件循环中,微任务队列会在 "当前宏任务执行完后立即清空",再执行下一个宏任务。

  • 例:

    js 复制代码
    console.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. 微任务的 "连续执行" 特性
  • 微任务执行过程中产生的新微任务,会被加入当前队列并立即执行,直到队列完全清空。

  • js 复制代码
    promise.resolve().then(() => {     
    
        console.log('微任务1');     
    
            Promise.resolve().then(
            () => console.log('微任务2')); // 新增微任务 
    
        }); 
    
    // 执行顺序:微任务1 → 微任务2(同批次执行)
5.调用栈的 "后进先出" 特性
  • 函数调用栈是 "栈结构",最后入栈的函数先执行(弹出)。

  • js 复制代码
    function a() {
        b(); 
    }
    
    function b() {
        c(); 
    }
    
    function c() {
        console.log('c'
        ); 
    }
    
    a(); 
    // 调用栈变化:a入栈 → b入栈 → c入栈 → c出栈 → b出栈 → a出栈// 输出:c(c先执行)
四、总结

JS 运行机制的核心是 "单线程 + 事件循环"

  1. 同步代码通过 "执行栈(LIFO)" 立即执行;
  2. 异步任务(宏 / 微)被放入对应队列,等待执行栈为空;
  3. 事件循环按 "1 个宏任务→所有微任务→(可选)UI 渲染" 的顺序循环,保证异步任务有序执行。

理解这一机制,能清晰解释 "为什么setTimeout回调会延迟""为什么微任务比宏任务先执行" 等问题,是优化异步代码(如避免长时间阻塞执行栈)的基础。

2:事件循环队列执行过程

一、初始化:执行栈与全局执行上下文
  1. 执行栈创建

    JS 引擎执行代码前,会创建一个执行栈(Call Stack),用于管理执行上下文(代码执行的环境)。执行栈是 "后进先出(LIFO)" 的栈结构,每次函数调用会入栈,执行完后出栈。

  2. 全局执行上下文入栈

    当开始执行一段脚本(如<script>标签内的代码),JS 引擎首先创建全局执行上下文(Global Execution Context),并将其推入执行栈(此时执行栈只有全局上下文)。

    全局执行上下文的创建分两个阶段:

    • 创建阶段

      • 进行 "变量提升":为所有var变量、函数声明分配内存,初始值设为undefined(函数声明会直接赋值为函数体,优先于变量提升)。
      • 确定this指向(浏览器中指向window,Node.js 中指向global)。
      • 构建作用域链:全局作用域作为最外层链(后续函数调用会在作用域链中添加自身作用域)。
    • 执行阶段 : 逐行执行代码,为变量赋值(替换undefined为实际值),执行函数调用等同步操作。

二、同步执行:函数调用与执行上下文入栈

当执行到函数调用时(如fn()),JS 引擎会创建函数执行上下文(Function Execution Context),并推入执行栈(位于栈顶,优先执行)。函数执行上下文的处理与全局上下文类似,但多了两个步骤:

  1. 创建阶段

    • 变量提升:为函数内的var变量、let/const变量(暂存死区)、函数参数、内部函数声明分配内存。
    • 确定this指向(取决于调用方式,如普通调用指向window,对象调用指向该对象)。
    • 构建作用域链:以 "当前函数作用域→父函数作用域→...→全局作用域" 的顺序链接(这是子函数能访问父函数变量的原因)。
  2. 执行阶段

    逐行执行函数内代码,为变量赋值,处理函数内的同步逻辑;若遇到嵌套函数调用(如fn()内调用childFn()),则重复上述步骤,将childFn的执行上下文推入栈顶。

三、执行栈清空:同步代码执行完毕

当所有同步代码(包括嵌套函数)执行完毕后,执行栈会依次弹出已完成的执行上下文(从栈顶的函数上下文开始,最后弹出全局上下文),此时执行栈为空。

四、异步任务:宿主环境处理与回调入队

同步执行过程中若遇到异步任务 (如setTimeoutfetchPromise.then),JS 引擎(单线程)不会阻塞等待,而是将异步逻辑交给宿主环境(浏览器 / Node.js)的其他线程处理(如浏览器的定时器线程、网络线程),具体流程:

  1. 异步任务触发
    • 若为宏任务 (如setTimeoutfetch):宿主线程(如定时器线程)会等待触发条件(如延迟时间到、请求响应),满足后将回调函数放入宏任务队列(按 "先进先出" 排序)。
    • 若为微任务 (如Promise.thenqueueMicrotask):宿主线程处理完同步逻辑后,将回调函数放入微任务队列(优先级高于宏任务队列)。
  2. 回调入队的时机
    • Promise.then:当Promise状态变为fulfilledrejected时,回调立即入微任务队列。

    • setTimeout:延迟时间到后,回调入宏任务队列(即使延迟 0ms,也需等待当前同步代码执行完)。

五、事件循环:执行栈→微任务→宏任务的循环

执行栈为空后,JS 引擎进入事件循环(Event Loop),核心流程如下:

  1. 处理微任务队列

    依次将微任务队列中的回调函数推入执行栈执行(遵循 "后进先出",但队列本身是 "先进先出")。

    • 若执行微任务过程中产生新的微任务(如then回调中又调用Promise.resolve().then()),新微任务会被加入当前队列末尾,同批次执行(直到微任务队列为空)。
  2. (浏览器特有)UI 渲染

    微任务队列清空后,浏览器会判断是否需要重新渲染(如 DOM 有更新),执行一次 UI 渲染(与requestAnimationFrame回调配合)。

  3. 处理宏任务队列: 从宏任务队列中取第一个回调函数推入执行栈执行(同步执行其内部代码,包括可能的函数调用、新的异步任务)。

  4. 循环往复

    该宏任务执行完→执行栈为空→处理微任务队列→(可选)UI 渲染→取下一个宏任务→... 直到所有任务执行完毕。

六、特殊场景补充
1. 闭包与执行上下文的关系

当父函数执行完毕并出栈后,若子函数仍被外部引用(如作为返回值),子函数的作用域链会保留对父函数作用域的引用(即闭包)。此时父执行上下文虽已出栈,但子函数仍能访问父函数的变量(变量内存未被 GC 回收,因闭包持有引用)。只有当子函数也执行完毕且无引用时,父变量才会被回收。

2. 异步回调的执行条件

所有异步回调(微任务 / 宏任务)必须等待执行栈为空时才会入栈执行。即使宏任务队列中有回调,若微任务队列未清空,也不会执行宏任务(微任务优先级更高)。

七、总结:事件循环完整流程
  1. 初始化:创建执行栈,全局执行上下文入栈,经历创建阶段(变量提升等)和执行阶段(同步代码执行)。
  2. 函数调用:函数执行上下文入栈,执行内部同步代码,嵌套函数会入栈在栈顶,执行完后出栈。
  3. 同步执行完毕:执行栈为空,所有同步代码处理完成。
  4. 处理微任务:将微任务队列所有回调入栈执行(包括新增微任务),直到队列为空。
  5. (浏览器)UI 渲染:若有 DOM 更新,执行一次渲染。
  6. 处理宏任务:从宏任务队列取第一个回调入栈执行,执行完后重复步骤 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 预定义的属性和方法:如parseIntMathJSON等,可直接访问(例如window.parseIntparseInt等价)。
  • 顶层代码中,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]]是引擎内部属性,不可通过代码访问)。

    例如:

    js 复制代码
    var 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)。

  • 严格模式thisundefined(避免全局变量污染)。

2. 函数执行上下文中的 this

函数中的this指向由调用方式决定,常见场景包括:

  • 普通函数调用 (如foo()):

    • 非严格模式this指向全局对象;
    • 严格模式thisundefined
  • 对象方法调用 (如obj.foo()):this指向调用方法的对象(obj)。

  • 构造函数调用 (如new Foo()):this指向新创建的实例对象。

  • 通过apply/call/bind调用this指向传入的第一个参数(若为非对象,会被自动转为对象类型,除非为null/undefined)。

四、执行上下文的生命周期与执行栈(ECStack)

执行上下文的生命周期分为创建阶段和执行阶段,由执行上下文栈(Execution Context Stack,ECStack)管理执行顺序。

1. 生命周期阶段
  • 创建阶段(进入执行上下文):

    完成三件事

    1. 确定变量对象(VO)/ 活动对象(AO)(初始化变量、函数声明);
    2. 构建作用域链(结合[[Scope]]和 AO);
    3. 确定this的指向。
  • 执行阶段

    执行代码,完成变量赋值、函数调用等操作(即 "逐行执行,为变量赋真实值")。

2. 执行上下文栈(ECStack)的管理规则

执行上下文栈是引擎用于管理执行上下文的 "栈结构",遵循后进先出(LIFO) 原则:

  • 脚本加载时,全局执行上下文首先入栈,且始终在栈底(直到程序结束才出栈)。

  • 函数被调用时,其执行上下文入栈,成为 "当前执行上下文"。

  • 函数执行完毕(或返回)时,其执行上下文出栈,栈顶指向之前的上下文(父级或全局)。

五、示例:全局与函数执行上下文的完整流程

以如下代码为例,拆解执行过程:

js 复制代码
var globalVar = "全局变量"; // 全局变量 

function checkScope() {   
    var scopeVar = "函数内变量"; // 函数内变量   
    return scopeVar;
} 

checkScope(); // 调用函数
步骤 1:全局执行上下文创建与执行
  • 创建阶段

    • 变量对象(VO) = 全局对象(window);
    • 初始化 VOglobalVar = undefinedcheckScope = 函数体(函数声明提升);
    • 作用域链 = [window]
    • this = window(非严格模式)。
  • 执行阶段

    • globalVar赋值:globalVar = "全局变量"

    • 执行checkScope(),触发函数执行上下文的创建。

步骤 2:checkScope 函数执行上下文入栈
  • 创建阶段
    1. 生成活动对象(AO):arguments = { length: 0 }scopeVar = undefined(变量声明提升);
    2. 构建作用域链:checkScope.[[Scope]] = [window] → 作用域链 = [AO, window]
    3. this = window(非严格模式,普通函数调用)。
  • 执行阶段
    • scopeVar赋值:scopeVar = "函数内变量"
    • 执行return scopeVar:通过作用域链在当前 AO 中找到scopeVar,返回其值。
步骤 3:函数执行上下文出栈

checkScope执行完毕,其执行上下文从 ECStack 中出栈,栈顶回到全局执行上下文。

六、补充说明
  1. 与事件循环的关联:全局执行上下文始终在 ECStack 中,直到所有代码(包括同步代码和异步任务回调)执行完毕才出栈,是事件循环中 "同步代码优先执行" 的基础。
  2. 闭包对作用域链的影响:若内部函数被外部引用(形成闭包),其作用域链会保留父级 AO(即使父函数已执行完毕),因此闭包可访问父函数的变量。
  3. ES6 的变化:ES6 中 "变量对象" 的概念被 "词法环境(Lexical Environment)" 和 "变量环境(Variable Environment)" 取代,但核心逻辑(变量存储、作用域管理)一致,可理解为 "更规范的 VO 实现"。

4:闭包补充说明

闭包是指那些能够访问,自由变量 的函数 自由变量 是指在函数 中使用的,但既不是函数参数也不是函数的局部变量的变量 组成闭包 = 函数 + 函数能够访问的自由变量
理论角度的闭包: 所有函数都是闭包。 为函数在创建时会捕获上层作用域的变量对象(即保存[[Scope]]),即使是最简单的全局函数,也能访问全局变量(自由变量),符合 "函数 + 自由变量" 的组成。
实践角度的闭包: 创建它的上下文已销毁 (如外部函数执行完毕并出栈); 仍能访问创建它时的自由变量 (即引用外部作用域的变量)。 核心 :闭包的本质是内部函数的作用域链 保留了对外部函数活动对象(AO)的引用,即使外部函数的执行上下文已从栈中移除,其 AO 因被引用而不会被垃圾回收(GC),从而使内部函数能持续访问这些变量。

一、闭包的核心定义与分类
1. 理论角度的闭包(ECMAScript 规范)
  • 定义 :所有函数都是闭包。因为函数在创建时会捕获上层作用域的变量对象(即保存[[Scope]]),即使是最简单的全局函数,也能访问全局变量(自由变量),符合 "函数 + 自由变量" 的组成。

  • js 复制代码
    var globalVar = 1;
    
    function foo() { 
        console.log(globalVar); 
    } 
    // foo是闭包:函数+自由变量globalVar

这里foo能访问全局变量globalVar(非参数 / 局部变量),因此是理论上的闭包。

2. 实践角度的闭包(开发中常用场景)
  • 定义:满足两个条件的函数:

    1. 创建它的上下文已销毁(如外部函数执行完毕并出栈);
    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"
执行流程与作用域链分析:
  1. checkscope 函数创建时 : 其内部属性[[Scope]] = [全局执行上下文的VO(window)](记录父级作用域)。
  2. checkscope 调用时
    • 进入函数执行上下文,创建活动对象AO{ scope: undefined, f: 函数体 }(变量声明提升)。
    • 作用域链 = [checkscope的AO].concat([[Scope]])[checkscopeAO, window]
    • 执行阶段:为scope赋值"local scope"f函数创建(此时f[[Scope]] = [checkscopeAO, window],记录当前作用域链)。
  3. checkscope 执行完毕
    • 其执行上下文从执行栈中弹出,但f[[Scope]]仍保留对checkscopeAO的引用(关键!)。
  4. 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. 常见用途
  • 模块化:封装私有变量(避免全局污染),如:

    js 复制代码
    function 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. letconst(块级作用域)
  • 核心改进 :引入块级作用域({} 内的作用域),解决 var 的三大问题:

    • 变量提升导致的 "声明前可访问"(var a = 1; if (true) { var a = 2; } 会污染外部);
    • 无块级作用域导致的循环闭包问题(如 for (var i=0;...)i 全局污染);
    • 可重复声明(var a=1; var a=2 不报错)。
  • 暂时性死区(TDZ) :变量在声明前不可访问(即使在 typeof 中),避免意外使用未初始化的变量:

    js 复制代码
    console.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 中参数默认值形成独立作用域,避免与函数体内变量冲突:

    js 复制代码
    function fn(x = y, y = 1) {} // 报错:x的默认值引用了未声明的y(y在参数作用域中)
二、对原有语法的增强

核心:简化代码编写,提升语法灵活性(如解构、模板字符串等)。

1. 解构赋值
  • 对象解构:支持嵌套解构、默认值、重命名:

    js 复制代码
    const user = { name: 'Alice', age: 20, addr: { city: 'Beijing' } };
    
    const { name: username, age = 18, addr: { city } } = user;
    // username = 'Alice',age = 20,city = 'Beijing'
  • 数组解构:支持 "跳过元素""剩余参数":

    js 复制代码
    const [a, , b, ...rest] = [1, 2, 3, 4, 5]; 
    // a=1,b=3,rest=[4,5]
2. 函数参数增强
  • 参数默认值 :直接在参数列表定义默认值,避免 arguments 或逻辑判断:

    js 复制代码
    function greet(name = 'Guest') { 
        console.log(`Hello, ${name}`); 
    }
    
    greet(); 
    // "Hello, Guest"
  • 剩余参数(...rest :替代 arguments(类数组),直接获取数组形式的参数:

    js 复制代码
    function sum(...nums) { 
        return nums.reduce((a, b) => a + b); 
    }
    
    sum(1, 2, 3); // 6(nums是真正的数组,支持数组方法)
3. 模板字符串
  • 多行文本 :直接换行,无需 \n

    js 复制代码
    const html = `  
        <div>  
            <p> 多行文本 </p>
        </div> 
    `;
  • 标签函数(Tagged Templates):自定义字符串处理逻辑(如过滤 HTML、国际化):

    js 复制代码
    function safeHTML(strings, ...values) {   
        // strings是模板字符串的静态部分,values是插值表达式结果   
        return strings.reduce((res, str, i) => res + str + (values[i] || ''), '');
    }
    
    const userInput = '';
    const safe = safeHTML`${userInput}`; // 可在此处过滤危险内容
4. 扩展运算符(...
  • 数组扩展 :浅拷贝、合并、转换类数组(如 NodeList):

    js 复制代码
    const arr1 = [1, 2];
    
    const arr2 = [...arr1, 3, 4]; // [1,2,3,4](合并)
    
    const nodes = document.querySelectorAll('div');
    
    const nodeArr = [...nodes]; // 转换为数组
  • 对象扩展:合并对象(后出现的属性覆盖前者)、复制属性:

    js 复制代码
    const 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. ProxyReflect(元编程)
  • Proxy:拦截对象的底层操作(如属性读写、函数调用),实现数据劫持、权限控制等:

    js 复制代码
    const 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 中复用默认逻辑:

    js 复制代码
    const 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(唯一标识符)
  • 特性:创建后的值唯一,即使描述相同也不相等,用于避免属性名冲突:

    js 复制代码
    const s1 = Symbol('id');
    
    const s2 = Symbol('id'); s1 === s2; // false(唯一)
  • 应用

    • 作为对象的私有属性键(无法通过 for...inObject.keys 枚举);

    • 定义常量(如枚举值):const STATUS = { PENDING: Symbol('pending'), DONE: Symbol('done') }

    • 为对象添加内置方法(如 Symbol.iterator 定义迭代器)。

2. SetWeakSet(集合)
  • Set:无重复值的集合,支持增删查:

    js 复制代码
    const 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. MapWeakMap(键值对集合)
  • Map:键可以是任意类型(对象也可作为键),有序(按插入顺序):

    js 复制代码
    const m = new Map();
    const objKey = { id: 1 }; 
    
    m.set(objKey, 'value'); // 以对象为键 
    m.get(objKey); // 'value'(通过对象获取值)
  • WeakMap:键必须是对象,且为弱引用(键对象被回收后,对应键值对自动删除),适合存储对象的附加信息(如 DOM 节点的元数据),不可遍历。

五、其他重要特性补充
1. 类与继承(classextends
  • 语法糖:简化 ES5 的原型链继承,提供更清晰的类定义:

    js 复制代码
    class 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. Generatorasync/await(异步流程控制)
  • Generator :函数通过 yield 暂停执行,next() 恢复,返回迭代器,可用于分步处理:

    js 复制代码
    function* 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 的语法糖,让异步代码像同步一样直观:

    js 复制代码
    async 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 } } }),如数组、SetMap 等默认可迭代。
  • for...of:遍历可迭代对象的值(替代 for 循环和 forEach):
js 复制代码
for (const num of [1, 2, 3]) {   
    console.log(num); // 1, 2, 3(遍历值)
}
六、总结

ES6 的新特性从语法、API、数据结构等多维度提升了 JavaScript 的表现力:

  • 解决了 var 作用域、回调地狱等历史问题;
  • 增强了语法灵活性(解构、扩展运算符等);
  • 引入了 PromiseProxy 等强大工具;
  • 新增了 SymbolSetMap 等数据类型,满足复杂场景需求。

这些特性是现代前端开发的基础,也是理解框架(如 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()):优点:简单快捷。缺点:无法处理 FunctionRegExpDate(会转为字符串)、循环引用(直接报错),适合纯 JSON 数据场景。

    js 复制代码
    const cloneObj = JSON.parse(JSON.stringify(originalObj));
二、完善版本实现方式
  • Date:通过 new Date() 复制时间戳。

  • RegExp:复制源文本(source)和修饰符(flags)。

  • Reflect.ownKeys(target):获取所有自身键(包括 Symbol 键),比 for...in 更全面。

  • Map:缓存克隆体,避免循环引用,如 obj.self = obj

    js 复制代码
    function 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:定时器

一、定时器基础:setTimeoutsetInterval 是什么?

setTimeoutsetInterval 是浏览器提供的 定时器 API,用于延迟或重复执行代码,属于 宏任务(由宿主环境发起,回调函数进入宏任务队列)。

1. setTimeout:延迟执行一次
  • 作用:指定延迟时间后,执行一次回调函数。

  • 语法

    js 复制代码
    const 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:间隔重复执行
  • 作用:每隔指定时间,重复执行回调函数。

  • 语法

    js 复制代码
    const 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. 取消定时器:clearTimeoutclearInterval
  • 作用 :通过 timerId 取消未执行的定时器(避免内存泄漏)。

  • 语法

    js 复制代码
    clearTimeout(timerId); // 取消 setTimeout
    
    clearInterval(timerId); // 取消 setInterval
  • 示例

    js 复制代码
    const timer = setTimeout(() => {   console.log('这行不会执行');
    
    }, 1000); clearTimeout(timer); // 立即取消
二、核心原理:定时器为什么 "不准时"?

定时器的延迟 / 间隔时间 不是精确的,因为受 JS 单线程和事件循环 影响:

  1. 回调进入宏任务队列:setTimeout/setInterval 的回调函数会被放入 宏任务队列,需等待当前执行栈清空(同步代码、微任务执行完毕)后才会执行。

  2. 延迟是 "最早执行时间":

    • 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 嵌套实现 "固定间隔"(确保前一次执行完再安排下一次):

    js 复制代码
    function repeatTask() {   
        console.log('执行任务');  
        // 任务完成后,再延迟1秒安排下一次   
        setTimeout(repeatTask, 1000);
    }
    
    repeatTask(); // 首次执行
四、常见误区与注意事项
1. delay=0 不代表 "立即执行"
  • setTimeout(callback, 0) 的回调会进入宏任务队列,需等待:

    • 当前同步代码执行完毕;
    • 所有微任务执行完毕。
  • 示例

    js 复制代码
    console.log('同步代码1');
    
    setTimeout(() => console.log('setTimeout'), 0);
    
    Promise.resolve().then(() => console.log('微任务'));
    
    console.log('同步代码2'); // 执行顺序:同步代码1 → 同步代码2 → 微任务 → setTimeout
2. 回调函数中的 this 指向
  • 非严格模式 :回调函数中 this 指向全局对象(window);

  • 严格模式thisundefined

  • 解决 :用箭头函数(继承外部 this)或 bind 绑定:

    js 复制代码
    const obj = {  
        name: '前端',   
        start() {     
            // 箭头函数继承 obj 的 this     
            setTimeout(() => {       
            console.log(this.name); // 输出 '前端'     
            }, 1000);   
        }
    }; 
    
    obj.start();
3. 定时器的 "最小延迟" 限制
  • 浏览器对 delay 有最小限制(通常 4ms),尤其是嵌套层级超过 5 层的定时器(为避免恶意代码占用资源)。

  • 例:多层嵌套的 setTimeout 延迟会被强制改为 4ms:

    js 复制代码
    let 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 次 / 秒),节省资源。
五、实践技巧:如何用好定时器?
  1. 避免长时间阻塞主线程:定时器回调应尽量轻量化, heavy 任务(如大数据处理)移至 Web Worker

  2. 及时取消定时器:组件销毁、页面卸载时,用 clearTimeout/clearInterval 取消定时器,避免内存泄漏:

    js 复制代码
    // 页面关闭前清理
    window.addEventListener('beforeunload', () => {   
        clearTimeout(timer);
    });
  3. 替代方案:

    • 动画场景:用 requestAnimationFrame(与渲染帧同步,更流畅);

    • 高精度计时:用 performance.now()(比 Date.now() 更精确)。

六、总结
  • setTimeout 用于 "延迟执行一次",setInterval 用于 "重复执行",均返回 timerId 用于取消。
  • 定时器是宏任务,回调执行时间受主线程繁忙程度影响,非绝对精确。
  • 注意 setInterval 的 "累积执行" 问题,优先用 setTimeout 嵌套实现可控间隔。

掌握定时器的原理和细节,能避免 "计时不准""内存泄漏" 等问题,在异步场景(如延迟加载、轮询)中更高效地使用。

9:requestAnimationFrame 与 setTimeout、setInterval 的区别

一、requestAnimationFrame 核心知识点

requestAnimationFrame 是浏览器提供的 专门用于动画绘制的 API,其设计初衷是解决传统定时器(setTimeout/setInterval)在动画场景中可能出现的卡顿、掉帧问题,让动画更流畅。

  1. 基本语法

    js 复制代码
    const animationId = requestAnimationFrame(callback);
  2. 使用示例

    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) 取消动画。
  3. 核心特性

    • 执行时机:由浏览器 在下一次重绘前调用,执行频率与浏览器刷新率保持一致(通常为 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 指向,并传入参数列表(逐个传入)。

  • 语法

    js 复制代码
    function.call(thisArg, arg1, arg2, ...);
    • thisArg:函数执行时 this 的指向(若为 null/undefined,非严格模式下 this 指向全局对象,严格模式下为 undefined)。
    • arg1, arg2...:传递给函数的参数(可选)。
  • 示例

    js 复制代码
    const 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 指向,并传入参数数组(或类数组对象)。

  • 语法

    js 复制代码
    function.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,且可预先传入部分参数(柯里化)。

  • 语法

    js 复制代码
    const 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)
五、注意事项
  1. thisArg 为基本类型时的处理: 若 thisArg 是字符串、数字、布尔值等基本类型,call/apply/bind 会自动将其转为对应的包装对象(如 StringNumber):

    js 复制代码
    function logThis() { 
        console.log(this); 
    } 
    logThis.call(123); // Number { 123 }(基本类型 123 被转为包装对象)
  2. bind 绑定的 this 不可修改: 用 bind 生成的新函数,其 this 被永久绑定,后续再用 call/apply 也无法修改:

    js 复制代码
    const 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 修改)
  3. 箭头函数不适用: 箭头函数没有自己的 this(继承外层上下文的 this),因此 call/apply/bind 无法改变其 this 指向(仅能传递参数):

    js 复制代码
    const arrowFn = () => { 
        console.log(this); 
    }; 
    arrowFn.call({ name: 'test' }); 
    // window(非严格模式下,this 仍指向外层上下文)
六、总结
  • 核心功能 :改变函数执行时的 this 指向,提升函数的灵活性和复用性。
  • 选择依据
    • 需立即执行 + 参数少:用 call(参数逐个传);
    • 需立即执行 + 参数多(数组形式):用 apply
    • 需延迟执行 + 固定 this 或固定部分参数:用 bind
  • 理解三者的区别,能更精准地处理函数上下文,避免 this 指向混乱导致的 bug。

11:JS设计模式

单例模式(Singleton)
  • 定义:确保一个类仅有一个实例,并提供全局访问点。

  • 核心思想:限制实例化次数,避免重复创建消耗资源的对象(如全局缓存、模态框)。

  • 示例

    js 复制代码
    class 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-elseswitch判断。

  • 示例:表单验证策略

    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)
  • 定义:提供创建对象的接口,隐藏具体实例化逻辑(根据参数返回不同类型对象)。

  • 核心思想:解耦对象创建与使用,简化复杂对象的创建过程。

  • 示例:简单工厂(创建不同类型的按钮)

    js 复制代码
    class 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框架内容,学前端的朋友别错过,咱们下期见。

更多

💻 前端项目实践:vue3-multi-platform

📊 AI 资源汇总:AI-Research-Radar

相关推荐
古希腊被code拿捏的神1 小时前
【Flutter】面试记录
flutter·面试·职场和发展
小飞悟1 小时前
那些年我们忽略的高频事件,正在拖垮你的页面
javascript·设计模式·面试
嘻嘻哈哈开森2 小时前
技术分享:深入了解 PlantUML
后端·面试·架构
爱学习的茄子2 小时前
JavaScript闭包实战:解析节流函数的精妙实现 🚀
前端·javascript·面试
Dgua2 小时前
🚀Promise 从入门到手写:核心方法实现全指南
前端·面试
Hilaku4 小时前
我为什么放弃了“大厂梦”,去了一家“小公司”?
前端·javascript·面试
然我4 小时前
React 事件机制:从代码到原理,彻底搞懂合成事件的核心逻辑
前端·react.js·面试
豆苗学前端4 小时前
从零开始教你如何使用 Vue 3 + TypeScript 实现一个现代化的液态玻璃效果(Glass Morphism)登录卡片
前端·vue.js·面试
阳火锅5 小时前
在生产环境下,你真的有考虑到使用数组方法的健壮性吗?
前端·javascript·面试
爱学习的茄子5 小时前
JavaScript闭包实战:防抖的优雅实现
前端·javascript·面试