2026-01-19-杂记 2026-01-19
最后更新时间:
页面浏览: 加载中...
闭包
函数+创建时的词法环境
当一个函数能够“记住并访问”它被创建时所处的词法作用域中的变量,即使这个函数在它原本的作用域之外被执行,这个函数就形成了闭包。
“闭包就是函数和其词法环境的组合”
“闭包就是能够读取其他函数内部变量的函数”
闭包是语言运行时的一种行为现象
1 | |
作用:封装,保存状态,延迟执行
判断:能否访问已销毁外部作用域的变量(基于函数定义位置)
闭包应用场景
- 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包:
1 | |
- 作为函数参数传递的形式:
1 | |
- IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量:
1 | |
IIFE 是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
- 结果缓存(备忘模式)
备忘模式就是应用闭包的特点的一个典型应用。比如下面函数:
1 | |
当多次执行 add() 时,每次得到的结果都是重新计算得到的,如果是开销很大的计算操作的话就比较消耗性能了,这里可以对已经计算过的输入做一个缓存。所以这里可以利用闭包的特点来实现一个简单的缓存,在函数内部用一个对象存储输入的参数,如果下次再输入相同的参数,那就比较一下对象的属性,如果有缓存,就直接把值从这个对象里面取出来。实现代码如下:
1 | |
使用 ES6 的方式实现:
1 | |
备忘函数中用 JSON.stringify 把传给 adder 函数的参数序列化成字符串,把它当做 cache 的索引,将 add 函数运行的结果当做索引的值传递给 cache,这样 adder 运行的时候如果传递的参数之前传递过,那么就返回缓存好的计算结果,不用再计算了,如果传递的参数没计算过,则计算并缓存 fn.apply(fn, args),再返回计算的结果。
循环输出问题
最后来看一个常见的和闭包相关的循环输出问题,代码如下:
1 | |
这段代码输出的结果是 5 个 6,那为什么都是 6 呢?如何才能输出 1、2、3、4、5 呢?
可以结合以下两点来思考第一个问题:
- setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
- 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
那如何按顺序依次输出 1、2、3、4、5 呢?
1)利用 IIFE
可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。
1 | |
可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。利用立即执行函数的入参来缓存每一个循环中的 i 值。
2)使用 ES6 中的 let
ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。
1 | |
可以看到,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。
3)定时器第三个参数
setTimeout 作为经常使用的定时器,它是存在第三个参数的。我们经常使用前两个,一个是回调函数,另外一个是定时时间,setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。那么结合第三个参数,调整完之后的代码如下:
1 | |
可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现想要的结果
高阶函数(Higher-Order Function)
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 | |
柯里化(Currying)
柯里化是将一个接受 多个参数 的函数,转换为 一系列只接受单个参数 的函数的变换过程。
f(a, b, c) === f(a)(b)(c)
把“一次传入多个参数” → 拆成“多次每次只传一个参数”的调用链。
1 | |
防抖(Debounce)
定义:当事件被触发后,延迟 n 秒再执行回调函数。如果在这 n 秒内事件再次被触发,则重新开始计时。
1 | |
**应用:
- 搜索框输入自动搜索(用户输入完再发请求)
- 表单验证(输入停止后才校验)
- 按钮短时间内多次点击(防止重复提交)
- window.resize 事件
节流(Throttle)
定义:规定时间内只执行一次函数,无论这段时间内触发多少次。
1 | |
应用场景:
- 页面滚动到底部加载更多(无限滚动)
- 拖拽元素时实时计算位置
- 鼠标移动时画板实时绘制
- 游戏中技能释放冷却(最典型的思想来源)
call、apply 和 bind
call、apply 和 bind 是 JavaScript 中 Function 对象提供的三个非常重要且经常被面试考察的方法,它们的主要作用都是改变函数执行时的 this 指向,但在用法、参数传递方式和执行时机上存在显著差异。
| 特性 | call | apply | bind |
|---|---|---|---|
| 主要功能 | 立即执行函数,并改变 this 指向 | 立即执行函数,并改变 this 指向 | 不立即执行,返回一个新函数,永久绑定 this |
| 参数传递方式 | 逐个参数列出 | 参数以数组形式传递 | 逐个参数列出(与 call 相同) |
| 返回值 | 函数执行后的返回值 | 函数执行后的返回值 | 新的绑定了 this 的函数 |
| 是否立即执行 | 是 | 是 | 否 |
| 参数是否可以后续补充 | 否(一次性传完) | 否(一次性传完) | 是(可以分两次传参) |
| 典型使用场景 | 需要明确控制 this 并立即执行时 | 处理类数组对象/动态参数时 | 事件处理、固定 this 的回调函数 |
| 语法示例 | fn.call(obj, 1, 2, 3) | fn.apply(obj, [1, 2, 3]) | const newFn = fn.bind(obj, 1, 2) |
1 | |
1. call()
立即执行,参数逐个传入
1 | |
2. apply()
立即执行,参数以数组形式传入(非常适合处理 arguments 或类数组对象)
1 | |
3. bind()
最重要的特点:不立即执行,而是返回一个永久绑定了 this 的新函数
1 | |
场景:
- 需要立刻执行 → 选 call 或 apply
- 处理数组/类数组参数 → 优先考虑 apply
- 需要保存一个固定 this 的函数(最常见:事件回调、setTimeout、React class 组件等)→ 选 bind
- 想同时固定 this 又预设部分参数(函数柯里化)→ 也用 bind
IIFE
IIFE是 Immediately Invoked Function Expression 的缩写
JavaScript 中一种非常常见且重要的设计模式,主要用于创建独立的作用域并立即执行代码,同时避免污染全局命名空间
ES6前常用
1 | |
事件循环
事件循环是 JavaScript 单线程运行时用来处理异步任务的调度器,它通过“同步代码 → 微任务 → 一个宏任务 → 微任务 → 下一个宏任务……”的循环,不断地把异步回调安排到合适的时机执行,从而实现了看似并发的效果
| 组成部分 | 英文名称 | 主要职责 | 执行时机 | 示例任务来源 |
|---|---|---|---|---|
| 调用栈 | Call Stack | 存放当前正在执行的同步代码,按 LIFO(后进先出)执行 | 立即执行 | 函数调用、代码块 |
| 微任务队列 | Microtask Queue | 存放优先级最高的异步回调 | 当前宏任务结束后立即清空 | Promise.then/catch/finally、queueMicrotask、MutationObserver |
| 宏任务队列 | Task Queue / Macrotask Queue | 存放普通的异步回调 | 微任务队列清空后取一个执行 | setTimeout、setInterval、setImmediate、I/O、UI rendering、requestAnimationFrame |
| Web APIs | (浏览器提供) | 处理真正的异步操作(定时器、网络请求、DOM事件等),完成后将回调放入队列 | 由浏览器/运行时独立线程管理 | XMLHttpRequest、fetch、DOM事件监听 |
| 事件循环(Event Loop) | Event Loop | 不停询问:调用栈是否为空?→ 是的话先清空微任务 → 再取一个宏任务执行 | 持续运行 | — |
事件循环一次完整循环的执行顺序
- 执行当前调用栈中的所有同步代码(直到调用栈清空)
- 清空微任务队列(执行所有微任务,直到微任务队列为空)
只要有微任务,就会一直执行完所有微任务 - 从宏任务队列中取一个任务执行(注意:只取一个!)
- 执行完这个宏任务后 → 回到第 2 步,再次清空微任务队列
- 重复 3~4 步,直到所有任务处理完毕
代码示例与执行顺序演示
1 | |
执行结果顺序:
1 | |
- 同步代码 → 打印 1、7
- 微任务队列有 then(5) → 执行 → 打印 5,并产生新的微任务 then(6)
- 继续清空微任务 → 执行 then(6) → 打印 6
- 微任务队列清空,执行第一个宏任务(setTimeout 0ms)→ 打印 2
- 宏任务执行中产生微任务 then(3) → 宏任务结束后立即清微任务 → 打印 3
- 微任务清空,继续下一个宏任务(setTimeout 里面的)→ 打印 4
| 序号 | 问题 | 回答 | |
|---|---|---|---|
| 1 | 项目里用了泛型,怎么根据传入类型适配不同处理函数 | 通常用泛型约束 + 映射类型 + 条件类型 + infer 实现函数重载效果 最常见两种做法: 1. 传入对象类型 → 用 keyof T 约束字段名 2. 传入联合类型 → 用条件类型分发实现不同分支 | |
| 2 | 泛型是什么 | 泛型(Generics) 是 TypeScript(以及许多现代编程语言)中一种强大的类型系统特性,其核心目的是允许开发者编写可复用、类型安全的代码,同时让代码在不同具体类型下都能保持正确的类型推断和约束。 简单来说,泛型就是“类型参数化”:把类型当作参数一样传入,让同一个函数、类、接口能够处理多种类型的数据,而无需为每种类型都重复编写代码。 |
|
| 3 | interface 和 type 的区别 | 现在差别已经很小,主要记住这几点: • interface 可多次声明自动合并(对库/模块声明补丁最有用) • type 可做联合、交叉、映射、条件、模板字符串等复杂类型操作 • interface 性能略好(声明合并时) • 目前绝大多数团队:能用 type 就用 type,除非明确需要声明合并 | |
| 4 | keyof、in、typeof、infer 实际工作中怎么用 | - keyof:取对象所有键名(表单字段校验、权限映射最常用) - in:映射类型必备(生成只读、部分、可选等类型) - typeof:获取值的类型(尤其是从 const 断言对象取类型) - infer:在条件类型中推断出某一部分类型(最强大,常用于提取 Promise 返回值、数组元素类型、函数返回值等) | |
| 5 | 封装表单组件,要求字段名必须是后端返回字段的一部分,怎么处理 | 最推荐做法(2024~2026 主流): ts type ApiResponse = { id: number; name: string; age: number; email?: string } type FormField |
${keyof T}.${string}function Form 进阶:用 const assertion + typeof 做更严格的路径类型 |
| 6 | 回流(reflow)和重绘(repaint)的区别 | 回流(Layout/Reflow):几何属性/布局变化 → 影响元素位置/大小 → 必须重新计算布局 重绘(Repaint):外观变化(颜色、visibility、阴影等)→ 不影响布局 回流代价远大于重绘(通常是重绘的几倍到几十倍) | |
| 7 | 浏览器加载一个页面时,整个渲染流程(关键节点) | 1. DNS → TCP → TLS 2. 请求 HTML 3. 构建 DOM + CSSOM 4. 合成 Render Tree 5. Layout(回流) 6. Paint(重绘) 7. Composite(图层合成) 重要:Parse → Style → Layout → Paint → Composite | |
| 8 | 做骨架屏时,怎么判断骨架内容和真实内容之间的切换时机 | 推荐组合方式(从激进到保守): 1. 最激进:第一个/几个关键接口请求完成就切 2. 常用:所有必须数据接口都 resolve 后切(Promise.all) 3. 保守:主图/首屏关键资源加载完成(img onload + 定时兜底) 4. 极致体验:分层渐进式替换(文字先换、图片后换) | |
| 9 | 移动端怎么做适配(2025~2026 主流方案) | 当前最主流组合: 1. postcss-px-to-viewport-8-plugin / postcss-px-to-rem + tailwindcss 2. 设计稿 375px → vw 方案(750 设计稿用 100vw = 750px) 3. 大屏/横屏特殊处理:100vh polyfill + env(safe-area-inset-*) 4. 字体:clamp() + rem(根字体 100px 常见) | |
| 10 | async 函数在执行栈中的位置是怎么样的 | async 函数本身是同步执行到第一个 await 第一个 await 之后 → 后续代码作为微任务放入微任务队列 所以:async 函数 = 同步前半段 + 多个微任务 | |
| 11 | Promise.all 和 Promise.allSettled 的区别 | Promise.all:有一个 reject 就整体 reject,结果顺序严格对应 Promise.allSettled:永远 resolve,返回每个 promise 的 {status, value/reason},永远不会 reject | |
| 12 | JS 是单线程,为什么可以实现并发加载多个接口? | JS 单线程指的是执行 JavaScript 代码的线程只有一个 真正的网络请求、定时器、DOM事件等都由浏览器底层多线程完成 JS 只负责注册回调,回调通过事件循环被调度执行 → 所以宏观上是并发 | |
| 13 | setTimeout 和 requestAnimationFrame 哪个更精确 | requestAnimationFrame 更精确(与浏览器刷新率同步,通常 16.7ms/60fps) setTimeout 实际延迟往往偏长(最低 4ms + 事件循环排队延迟) 动画/滚动相关优先使用 rAF | |
| 14 | 帧率监控你们是怎么做的 | 主流几种方式(按精度/成本排序): 1. requestAnimationFrame 循环 + performance.now() 差值(最常用) 2. PerformanceObserver + ‘longtask’(监控长任务) 3. Web Vitals(CLS、FID、LCP) 4. 第三方:sentry、ddtrace、阿里云 ARMS 等 | |
| 15 | 小程序渲染卡顿时怎么优化(微信/支付宝小程序) | 优先级排序(效果最明显→一般): 1. setData 合并 + 减少次数(一次 setData 尽量多字段) 2. 使用 block wx:if 代替大量 wx:if 3. 列表用 virtual-list / recycle-view 4. 图片用 webp + lazy-load 5. 避免在循环里写 setData 6. 自定义组件纯数据组件化 | |
| 16 | H5 页面嵌入小游戏时,遇到过页面内存持续上涨吗?怎么解决的? | 非常常见,尤其 canvas + webgl 小游戏 常见原因: • 事件监听未移除 • canvas 未清理(ctx.clearRect 不够) • 纹理/缓冲区未释放 • 闭包引用大对象 • requestAnimationFrame 未取消 解决办法: 1. 组件卸载时务必 cancelAnimationFrame 2. 清理纹理:gl.deleteTexture/gl.deleteBuffer 3. 用 WeakMap/WeakSet 存引用 4. 定时检查 performance.memory(chrome devtools) 5. 必要时主动销毁整个 canvas 元素再重建 |
JS中异步的概念是什么?
JavaScript异步是指代码的执行不必等待某个操作完成,可以继续执行后续代码。当异步操作完成后,通过回调函数、Promise或async/await来处理结果。这是为了解决JS单线程可能导致的阻塞问题。
核心机制:事件循环 + 回调队列
- 同步任务在主线程执行
- 异步任务交由Web APIs处理
- 完成后的回调进入任务队列
- 事件循环从队列中取出回调执行
为什么JS是单线程还需要事件循环?
看似矛盾实则巧妙的设计:
- 单线程的必然性:避免多线程的复杂性(锁、竞态条件)
- 事件循环的价值:
- 处理I/O密集型操作(网络、文件)
- 维持UI响应性
- 管理定时器、用户交互
- 实现”非阻塞”的并发
简单比喻:就像餐厅里只有一个服务员(单线程),但通过叫号系统(事件循环)可以高效服务多桌客人。
栈内存和堆内存的差异?
| 特性 | 栈内存 | 堆内存 |
|---|---|---|
| 存储内容 | 基本类型、引用地址 | 引用类型(对象、数组) |
| 分配方式 | 自动分配/释放 | 动态分配,GC回收 |
| 大小 | 固定,较小 | 动态,较大 |
| 访问速度 | 快 | 慢 |
| 管理 | 系统自动管理 | 开发者/GC管理 |
| 示例 | let a = 1 | let obj = {x: 1} |
为什么数组要存在堆内存而不是栈内存?
根本原因:
- 大小不确定:数组长度动态变化,栈内存固定大小无法满足
- 性能考虑:复制大数组到栈中成本高
- 内存管理:堆内存可动态扩展,GC负责回收
- 引用语义:多个变量可共享同一数组
JS中有哪些基本数据类型?
7种基本类型:
- String
- Number
- Boolean
- Undefined
- Null
- Symbol(ES6)
- BigInt(ES2020)
1种引用类型:Object(包括Array、Function、Date等)
函数内部的this含义是什么?
this的指向规则:
- 默认绑定:独立函数调用 → window/undefined(严格模式)
- 隐式绑定:作为对象方法调用 → 该对象
- 显式绑定:call/apply/bind → 指定的对象
- new绑定:构造函数调用 → 新创建的对象
- 箭头函数:无自己的this,继承外层
call、apply、bind的区别?
| 方法 | 参数传递 | 立即执行 | 返回 |
|---|---|---|---|
| call | 参数列表 | 是 | 函数结果 |
| apply | 数组 | 是 | 函数结果 |
| bind | 参数列表 | 否 | 新函数 |
1 | |
TypeScript联合类型和交叉类型的概念?
联合类型 (Union Types):|表示”或”
1 | |
交叉类型 (Intersection Types):&表示”且”
1 | |
区别:
- 联合:可接受任一类型
- 交叉:必须同时满足所有类型
二、浏览器/网络类
什么是同源策略?
同源:协议、域名、端口完全相同
限制内容:
- DOM访问
- Cookie/LocalStorage访问
- AJAX请求
- 脚本执行
目的:防止恶意网站窃取用户数据
如何实现跨域请求?
常用方案:
- CORS:服务器设置响应头
- JSONP:利用
<script>标签 - 代理服务器:同源服务器转发
- WebSocket:不受同源限制
- postMessage:窗口间通信
- nginx反向代理:服务器端代理
为什么代理可以绕过同源限制?
核心原理:
- 同源限制是浏览器的安全策略,不是服务器的
- 代理服务器与页面同源,不受限制
- 服务器间无同源限制,可自由通信
流程:
1 | |
Cookie和Session的定义和差异?
| 特性 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 安全性 | 较低(可篡改) | 较高 |
| 存储大小 | 4KB左右 | 更大 |
| 生命周期 | 可设置过期时间 | 依赖服务器配置 |
| 跨域 | 可设置domain | 依赖Cookie或token |
Cookie是怎么种下的?
两种方式:
- HTTP响应头:
Set-Cookie: name=value; options - JavaScript设置:
document.cookie = "name=value; options"
关键属性:
HttpOnly:禁止JS访问Secure:仅HTTPS传输SameSite:限制跨站发送Domain/Path:作用域
LocalStorage和SessionStorage的区别?
| 特性 | LocalStorage | SessionStorage |
|---|---|---|
| 生命周期 | 永久,需手动清除 | 标签页关闭即清除 |
| 作用域 | 同源窗口共享 | 仅当前标签页 |
| 存储大小 | 5-10MB | 5-10MB |
| 数据同步 | 同源窗口实时同步 | 仅当前标签页 |
HTTP强缓存和协商缓存的区别?
强缓存:
- 响应头:
Cache-Control、Expires - 状态码:200 (from cache)
- 过程:不请求服务器,直接使用缓存
协商缓存:
- 响应头:
ETag/If-None-Match、Last-Modified/If-Modified-Since - 状态码:304 (Not Modified)
- 过程:请求服务器,验证缓存有效性
301和302状态码的区别?
301 Moved Permanently:
- 永久重定向
- 搜索引擎更新索引
- 浏览器缓存新地址
- 权重传递
302 Found (临时重定向):
- 临时重定向
- 搜索引擎不更新
- 浏览器不缓存
- 权重不传递
实际使用:
- 网站改版用301
- 登录跳转用302
- 移动端适配用302
三、CSS类
1. 设备像素和逻辑像素的区别?
设备像素 (Physical Pixel):
- 物理像素,屏幕实际发光点
- 固定不变
- 高DPI设备像素更密集
逻辑像素 (CSS Pixel):
- 编程使用的像素单位
- 与实际像素有比例关系
- 由
devicePixelRatio决定
关系:物理像素 = 逻辑像素 × devicePixelRatio
2. CSS两种盒模型的差异
标准盒模型 (content-box):
width/height= 内容宽度- 总宽度 = width + padding + border + margin
IE盒模型 (border-box):
width/height= 内容 + padding + border- 总宽度 = width + margin
设置方式:
1 | |