数据类型
- 基本数据类型 undefined、null、boolean、string、number
 - 引用数据类型 object、function、array
 - 独一无二不可变唯一类型 symbol
 - 数字类型,任意精度格式整数,超出number整数类型安全范围 bigint
 
- 判断数据类型
 
- typeof
 - instanceof
 - constructor
 - object.prototype.toString.call()
 
- 判断数组
 
- object.prototype.toString.call()
 - isArray
 - array.prototype.isPrototypeOf()
 
async和defer
html4.0中定义了defer;html5.0中定义了async。
如果没有defer和async,浏览器会立即加载并执行指定的JS脚本,并不会等待后续载入的文档元素。
如果有async,加载后续文档元素的过程中,将和JS的加载与执行,并行进行(异步)。
如果有defer,加载后续文档元素的过程中,将与JS的加载,并行进行(异步)。但JS的执行,要等到所有的文档元素解析完成之后,DOMContentLoaded事件触发之前完成。
动态创建script
在没有defer和async之前,异步加载的方式是动态创建script标签。
通过window.onload方法来确保页面加载完毕之后,再将script标签插入到页面中。
function addScriptTag(src) {
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.src = src;
    document.body.appendChild(script);
}
window.onload = function() {
    addScriptTag('./xxx.js');
}
垃圾回收&内存泄漏及优化
JavaScript 的垃圾回收(Garbage Collection, GC)是自动管理内存的核心机制,目标是识别并释放不再被使用的对象,避免内存泄漏。以下从原理、算法、优化策略三个维度深入解析:
一、核心原理:可达性分析
现代 JavaScript 引擎(如 V8、SpiderMonkey)普遍采用 ** 标记 - 清除(Mark-and-Sweep)** 算法,核心逻辑如下:
- 根对象(Roots):
 
- 全局变量、当前执行栈中的变量、闭包引用的变量等。
 - 引擎从根对象出发,递归遍历所有可达对象(Mark 阶段)。
 
- 标记阶段:
 
- 所有从根对象可达的对象被标记为 “存活”。
 - 不可达的对象(如未被引用的局部变量)被标记为 “垃圾”。
 
- 清除阶段:
 
- 回收所有未被标记的对象,释放内存空间。
 
示例:
function createObject() {
   const obj = { id: 1 }; // obj为局部变量,函数执行完毕后不可达
}
createObject(); // 函数执行后,obj成为垃圾,等待回收
二、主流算法与优化技术
- 分代回收(Generational GC)
 
V8 引擎将内存分为新生代和老生代,针对不同生命周期的对象采用不同策略:
- 新生代(Young Generation):
- 特点:存活时间短(如临时变量、函数作用域内的对象)。
 - 算法:Scavenge 算法(复制算法)。
- 内存分为From和To两个空间,每次只使用其中一个。
 - 存活对象被复制到To空间,From空间整体释放。
 - 多次存活的对象晋升到老生代(晋升阈值由引擎动态调整)。
 
 - 过程:分为两个区 1使用区2.空闲区
- 区分活动对象和非活动对象 活动对象标记
 - 标记的对象复制到空闲区,并内存排序 避免造成内存碎片
 - 清理非活动区的对象,释放内存
 - 两区互换,达到内存清理、整理的目的,当新的使用区占满时候再进行一次垃圾清理
 - 两次没有被回收的对象进入老生代
 
 
 - 老生代(Old Generation):
- 特点:存活时间长(如全局变量、长生命周期对象)。
 - 算法:标记 - 清除(Mark-Sweep) + 标记 - 整理(Mark-Compact)。
- 标记 - 清除:清除垃圾后可能产生内存碎片。
 - 标记 - 整理:将存活对象移动到内存一端,消除碎片。
 
 - 过程:使用主垃圾回收器(由于老生代内存比较大没办法分区域)
- 标记从根元素开始,递归根元素,过程中能达到元素的为活动对象,没有达到就是垃圾数据
 - 清除垃圾数据
 - 标记清除后的对象会产生内存碎片,所以采用标记整理(会讲活动对象向一端移动排序,从而避免产生碎片)
 
 
 
问题
- 由于v8是单线程语言,运行在主线程,在垃圾回收时会造成阻塞 (全停顿)
 
优化效果:
- 新生代回收效率提升 30%+(复制算法避免碎片化)。
 - 老生代通过增量标记(Incremental Marking)和并行回收(Parallel GC)减少主线程阻塞。
- 增量标记(子任务回收)
- V8引擎将一次完整的垃圾回收任务分成多个小的子任务,与JS交替执行
 
 - 并行回收
- 垃圾回收器在主线程执行中,开启多个辅助进程,同时进行这样的工作
 
 
 - 增量标记(子任务回收)
 
- 引用计数(Reference Counting)
 
- 原理:每个对象维护一个引用计数器,引用增加时 + 1,减少时 - 1,计数为 0 时回收。
 - 缺陷:无法处理循环引用。
 
   const objA = { name: 'A' };
   const objB = { name: 'B' };
   objA.ref = objB; // 相互引用,引用计数均为1
   objB.ref = objA;
   // 即使objA和objB不再被外部引用,引用计数仍为1,导致内存泄漏
- 现状:现代引擎已弃用,仅用于部分场景(如 Web Workers)。
 
三、内存泄漏与典型场景
- 
未清理的定时器
function startTimer() { setInterval(() => { console.log('定时任务'); }, 1000); } startTimer(); // 定时器未清理,回调函数持续引用外部变量解决方案: 使用clearInterval清除定时器。
 - 
闭包滥用
function createClosure() { const largeArray = new Array(1000000).fill(0); // 大数组 return function() { return largeArray[0]; // 闭包引用largeArray,导致无法回收 }; } const closure = createClosure(); // closure执行后,largeArray仍被引用解决方案: 减少闭包对外部变量的依赖,或在不需要时手动置为null。
 - 
无效的 DOM 引用
const div = document.createElement('div'); document.body.appendChild(div); div.dataset.largeData = new ArrayBuffer(1024 * 1024); // 存储大数据 document.body.removeChild(div); // div节点被移除,但data属性仍引用数据解决方案: 移除 DOM 节点前,手动释放其引用的资源。
 - 
全局变量污染
function leak() { leakVar = '全局变量'; // 未声明的变量自动成为全局变量 } leak(); // leakVar无法被回收解决方案: 严格模式('use strict')下禁止隐式全局变量。
 - 
对象循环引用
const objA = { name: 'A' }; const objB = { name: 'B' }; objA.ref = objB; // 相互引用,引用计数均为1 objB.ref = objA; // 引用计数视角:每个对象的引用计数为 1(自身)+ 1(对方引用)= 2,无法归零。 // 标记 - 清除视角:若 objA 和 objB 是全局变量(根对象可达),GC 会标记它们为 “存活”,即使它们的相互引用是唯一依赖解决方案:
- 若后续执行 objA = null; objB = null,全局引用被移除,但 局部作用域中的循环引用(如函数内的循环引用)不会泄漏(因根引用消失,GC 可回收)。
 
 
- 
如何避免循环引用导致的泄漏?
- 
手动断开引用
在对象不再使用时,将循环引用的属性置为 null:
// 断开 DOM 元素的循环引用 divA.parent = null; divB.remove(); // 同时从父节点移除 - 
使用弱引用数据结构
WeakMap/WeakSet:键或值为弱引用,不影响对象的垃圾回收:
const weakMap = new WeakMap(); const obj = { ref: {} }; weakMap.set(obj, 'data'); // obj 被回收时,WeakMap 自动移除条目 - 
避免全局变量存储循环引用
将对象限制在函数作用域内,确保根引用消失后,循环引用整体不可达:
function safeCycle() { const a = { ref: null }; const b = { ref: a }; a.ref = b; // 局部作用域内的循环,函数执行完后可回收 } 
 - 
 - 
总结:循环引用泄漏的核心条件
- 形成闭环:对象 A → B → A(或更多对象形成环)。
 - 根引用存在:环中的任意对象被全局变量、闭包或 DOM 树引用(GC 认为环整体可达)。
 
 - 
代码验证泄漏:
在 Chrome DevTools 的 Console 中执行以下代码,观察内存变化:
// 全局循环引用(导致泄漏) window.leak = (function() { const a = { ref: null }; const b = { ref: a }; a.ref = b; return { a, b }; // 作为根引用暴露 })(); // 手动释放根引用(断开泄漏) window.leak = null; // 此时 a 和 b 的环无任何根引用,GC 可回收 - 
理解循环引用的本质是 “根可达的环”,通过控制作用域和合理使用弱引用,可有效避免此类内存泄漏。
 
四、性能优化策略
- 减少不必要的内存分配
 
- 避免频繁创建临时对象:
// 反例:每次循环创建新对象 for (let i = 0; i < 1000; i++) { const obj = { value: i }; process(obj); } // 正例:复用对象 const obj = {}; for (let i = 0; i < 1000; i++) { obj.value = i; process(obj); } 
- 合理使用弱引用
 
- 
WeakMap:键为弱引用,键对象被回收时自动移除条目。
const cache = new WeakMap(); function process(obj) { if (!cache.has(obj)) { cache.set(obj, expensiveComputation(obj)); } return cache.get(obj); } // obj不再被引用时,WeakMap自动释放对应的缓存 - 
WeakSet:存储弱引用对象,适用于跟踪临时对象。
const visited = new WeakSet(); function visit(obj) { if (!visited.has(obj)) { visited.add(obj); // 处理逻辑 } } 
- 分代优化
 
- 新生代优化:
- 减少大对象在新生代的分配(如使用ArrayBuffer时预分配空间)。
 - 避免频繁创建短期存活的对象。
 
 - 老生代优化:
- 减少全局变量的使用,避免对象长期存活。
 - 避免频繁修改对象属性(可能触发老生代回收)。
 
 
- 工具辅助
 
- Chrome DevTools:
- Memory 面板:通过快照对比分析内存泄漏。
 - Performance 面板:跟踪垃圾回收时间和内存分配。
 
 - Node.js:
- 使用--trace-gc参数打印垃圾回收日志。
 - 通过process.memoryUsage()监控内存使用。
 
 
五、前沿特性:FinalizationRegistry
ES2022 引入的FinalizationRegistry允许在对象被回收时执行清理操作:
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`清理资源:${heldValue}`);
});
function createResource() {
  const resource = { id: Date.now() };
  registry.register(resource, `资源-${resource.id}`);
  return resource;
}
const res = createResource();
res = null; // 手动释放引用,触发回收
// 当resource被回收时,打印“清理资源:资源-1629782400000”
总结:垃圾回收的核心逻辑
- 自动回收:引擎通过可达性分析识别垃圾,无需手动干预。
 - 分代策略:新生代快速回收短期对象,老生代优化长期对象。
 - 避免泄漏:关注闭包、定时器、全局变量等常见泄漏源。
 - 工具与实践:结合 DevTools 和弱引用数据结构,提升内存管理效率。
 
- 理解这些机制后,开发者可更精准地优化代码,避免因内存问题导致的性能瓶颈。例如,在高频操作场景中优先使用栈内存(如基本类型),在复杂数据管理中合理运用WeakMap,并通过性能工具定位内存泄漏点。
 
requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
requestAnimationFrame比起setTimeout、setInterval的优势主要有亮点:
- requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率;
 
setTimeout、setInterval它们的内在运行机制决定了 时间间隔参数 实际上只是指定了把动画代码添加到 浏览器UI线程队列 中以等待执行的时间。如果队列前面已经加入了其它任务,那动画代码就要等前面的 任务完成后 再执行,并且如果时间间隔过短(小于16.7ms)会造成丢帧,所以就会导致动画可能不会按照预设的去执行,降低用户体验。
- 
在隐藏或不可见的元素中,将不会进行重新重绘或回流;
 - 
requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销
 
requestIdleCallback
RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。故RequestIdleCallback 定位处理的是: 不重要且不紧急的任务。
- 
缺点 这是一个实验中的功能 此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀。由于该功能对应的标准文档可能被重新修订,所以在未来版本的浏览器中该功能的语法和行为可能随之改变。
 - 
实验过程实验结论: requestIdleCallback FPS只有20ms,正常情况下渲染一帧时长控制在16.67ms (1s / 60 = 16.67ms)。该时间是高于页面流畅的诉求。
 
有人认为 RequestIdleCallback 主要用来处理不重要且不紧急的任务,因为React渲染内容,并非是不重要且不紧急。不仅该api兼容一般,帧渲染能力一般,也不太符合渲染诉求,故React 团队自行实现
- 
react-fiber blog.csdn.net/web20220509…