在 JavaScript 中,函数内部创建对象是一种非常常见的写法:
typescript
function createMessage(type, payload) {
return {
type,
payload,
timestamp: Date.now()
};
}
for (let i = 0; i < 100000; i++) {
const message = createMessage("event", i);
// 使用 message
}
这段代码本身没有问题。但如果函数被高频调用,并且每次都会创建临时对象,就可能产生大量短生命周期对象。它们会频繁进入垃圾回收流程,在某些场景下造成:
- 内存分配次数增加;
- Minor GC 频繁执行;
- 游戏或动画帧出现卡顿;
- 高频数据处理吞吐量下降;
- 移动端设备出现更明显的性能波动。
对象池(Object Pool)是一种通过"重复使用对象"来减少重复分配的优化技术。它预先创建或按需创建一批对象,使用时从池中获取,使用完成后归还池中,而不是每次都创建新对象。
不过需要注意:现代 JavaScript 引擎的垃圾回收已经非常高效。对象池并不是所有场景都适合,必须经过性能分析后再使用。
一、对象池的基本思想
普通方式:
ini
function createVector(x, y) {
return { x, y };
}
for (let i = 0; i < 100000; i++) {
const vector = createVector(i, i);
// 使用 vector
}
对象池方式:
ini
const vectorPool = [];
function acquireVector() {
return vectorPool.pop() || { x: 0, y: 0 };
}
function releaseVector(vector) {
vector.x = 0;
vector.y = 0;
vectorPool.push(vector);
}
for (let i = 0; i < 100000; i++) {
const vector = acquireVector();
vector.x = i;
vector.y = i;
// 使用 vector
releaseVector(vector);
}
核心流程是:
rust
获取对象 -> 设置数据 -> 使用对象 -> 清理状态 -> 归还对象
与其反复执行:
rust
创建对象 -> 使用对象 -> 等待 GC 回收
对象池将大量短生命周期对象变成了少量可重复使用的对象。
二、一个通用对象池实现
可以先实现一个通用对象池。
kotlin
class ObjectPool {
constructor(factory, reset, initialSize = 0, maxSize = Infinity) {
this.factory = factory;
this.reset = reset;
this.maxSize = maxSize;
this.items = [];
for (let i = 0; i < initialSize; i++) {
this.items.push(factory());
}
}
acquire() {
return this.items.pop() || this.factory();
}
release(object) {
this.reset(object);
if (this.items.length < this.maxSize) {
this.items.push(object);
}
}
get size() {
return this.items.length;
}
}
使用示例:
ini
const userPool = new ObjectPool(
() => ({
id: 0,
name: "",
score: 0,
active: false
}),
user => {
user.id = 0;
user.name = "";
user.score = 0;
user.active = false;
},
10,
100
);
获取和释放对象:
ini
const user = userPool.acquire();
user.id = 1001;
user.name = "Tom";
user.score = 90;
user.active = true;
// 使用 user
userPool.release(user);
这里的 reset 非常重要。如果不重置对象状态,下次获取时可能残留上一次使用的数据。
三、为什么必须重置对象状态
错误示例:
ini
const pool = [];
function acquireUser() {
return pool.pop() || {
name: "",
age: 0
};
}
function releaseUser(user) {
pool.push(user);
}
const user1 = acquireUser();
user1.name = "Alice";
user1.age = 18;
releaseUser(user1);
const user2 = acquireUser();
console.log(user2);
// { name: "Alice", age: 18 }
user2 继承了 user1 的数据,这可能导致非常隐蔽的业务 bug。
正确做法:
ini
function releaseUser(user) {
user.name = "";
user.age = 0;
pool.push(user);
}
或者在获取时重置:
ini
function acquireUser() {
const user = pool.pop() || {
name: "",
age: 0
};
user.name = "";
user.age = 0;
return user;
}
一般建议在 release 阶段完成清理,因为归还对象时状态更明确。
四、应用场景一:游戏中的粒子系统
粒子系统通常会在每一帧创建、更新和销毁大量粒子,例如:
- 爆炸效果;
- 烟雾;
- 火焰;
- 雨雪;
- 子弹轨迹;
- 魔法特效。
如果每个粒子都通过 new 创建,可能产生大量短生命周期对象。
4.1 不使用对象池
javascript
function createParticle(x, y) {
return {
x,
y,
vx: Math.random() * 2 - 1,
vy: Math.random() * 2 - 1,
life: 1,
size: 4
};
}
const particles = [];
function emitParticle(x, y) {
particles.push(createParticle(x, y));
}
大量创建和删除粒子时,会产生较多垃圾对象。
4.2 使用对象池
ini
class ParticlePool {
constructor(size = 1000) {
this.pool = [];
for (let i = 0; i < size; i++) {
this.pool.push(this.create());
}
}
create() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
size: 0,
active: false
};
}
acquire(x, y) {
const particle = this.pool.pop();
if (!particle) {
return null;
}
particle.x = x;
particle.y = y;
particle.vx = Math.random() * 2 - 1;
particle.vy = Math.random() * 2 - 1;
particle.life = 1;
particle.size = 4;
particle.active = true;
return particle;
}
release(particle) {
particle.x = 0;
particle.y = 0;
particle.vx = 0;
particle.vy = 0;
particle.life = 0;
particle.size = 0;
particle.active = false;
this.pool.push(particle);
}
}
粒子系统:
ini
const particlePool = new ParticlePool(1000);
const activeParticles = [];
function emit(x, y, count) {
for (let i = 0; i < count; i++) {
const particle = particlePool.acquire(x, y);
if (particle) {
activeParticles.push(particle);
}
}
}
function update(deltaTime) {
for (let i = activeParticles.length - 1; i >= 0; i--) {
const particle = activeParticles[i];
particle.x += particle.vx * deltaTime;
particle.y += particle.vy * deltaTime;
particle.life -= deltaTime;
if (particle.life <= 0) {
activeParticles.splice(i, 1);
particlePool.release(particle);
}
}
}
这种模式适合粒子数量相对可控、粒子结构固定、生命周期明确的系统。
五、应用场景二:游戏中的子弹和敌人对象
子弹通常具有明显的生命周期:
rust
创建子弹 -> 飞行 -> 命中或超出范围 -> 销毁
可以使用对象池避免频繁创建和销毁。
ini
class BulletPool {
constructor(maxSize = 500) {
this.pool = [];
for (let i = 0; i < maxSize; i++) {
this.pool.push(this.createBullet());
}
}
createBullet() {
return {
x: 0,
y: 0,
speed: 0,
angle: 0,
damage: 0,
active: false
};
}
acquire(config) {
const bullet = this.pool.pop();
if (!bullet) {
return null;
}
bullet.x = config.x;
bullet.y = config.y;
bullet.speed = config.speed;
bullet.angle = config.angle;
bullet.damage = config.damage;
bullet.active = true;
return bullet;
}
release(bullet) {
bullet.active = false;
bullet.x = 0;
bullet.y = 0;
bullet.speed = 0;
bullet.angle = 0;
bullet.damage = 0;
this.pool.push(bullet);
}
}
使用:
php
const bulletPool = new BulletPool(500);
const bullets = [];
function fire() {
const bullet = bulletPool.acquire({
x: 100,
y: 200,
speed: 800,
angle: 0,
damage: 10
});
if (bullet) {
bullets.push(bullet);
}
}
在游戏中,也可以用"固定数组 + active 标记"替代 push 和 splice,减少数组操作:
javascript
const bullets = Array.from({ length: 500 }, () => ({
x: 0,
y: 0,
active: false
}));
function acquireBullet() {
return bullets.find(bullet => !bullet.active);
}
function releaseBullet(bullet) {
bullet.active = false;
}
不过 find 每次都要遍历数组,性能要求高时可以维护空闲索引,或者使用专门的空闲列表。
六、应用场景三:高频数学计算对象
在动画、物理计算、地图渲染中,经常会产生大量临时向量:
css
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
如果该函数位于高频循环中,可能产生大量临时对象。
6.1 通过输出参数避免分配
一种简单方案是不返回新对象,而是将结果写入指定对象:
ini
function addTo(out, a, b) {
out.x = a.x + b.x;
out.y = a.y + b.y;
return out;
}
const result = { x: 0, y: 0 };
addTo(result, { x: 1, y: 2 }, { x: 3, y: 4 });
console.log(result);
// { x: 4, y: 6 }
这种方式通常比对象池更简单,也更容易控制对象生命周期。
6.2 使用向量对象池
ini
const vectorPool = new ObjectPool(
() => ({ x: 0, y: 0 }),
vector => {
vector.x = 0;
vector.y = 0;
},
32,
128
);
function addVector(a, b) {
const result = vectorPool.acquire();
result.x = a.x + b.x;
result.y = a.y + b.y;
return result;
}
const result = addVector(
{ x: 10, y: 20 },
{ x: 5, y: 8 }
);
console.log(result);
vectorPool.release(result);
但要特别注意:调用者必须明确知道什么时候释放 result。如果业务代码无法可靠管理生命周期,直接使用输出参数通常更安全。
七、应用场景四:网络协议和日志处理
在 WebSocket、消息队列、日志解析等场景中,可能每秒处理大量消息:
lua
function parseMessage(raw) {
return {
type: raw.type,
id: raw.id,
payload: raw.payload,
receivedAt: Date.now()
};
}
如果消息处理非常频繁,可以复用消息对象:
ini
const messagePool = new ObjectPool(
() => ({
type: "",
id: 0,
payload: null,
receivedAt: 0
}),
message => {
message.type = "";
message.id = 0;
message.payload = null;
message.receivedAt = 0;
},
100,
1000
);
function handleRawMessage(raw) {
const message = messagePool.acquire();
message.type = raw.type;
message.id = raw.id;
message.payload = raw.payload;
message.receivedAt = Date.now();
try {
processMessage(message);
} finally {
messagePool.release(message);
}
}
这里使用 try...finally 可以保证即使处理过程中抛出异常,对象也能归还对象池。
但是,这种方式有一个限制:processMessage 不能异步持有该对象。
错误示例:
javascript
function processMessage(message) {
setTimeout(() => {
console.log(message.type);
}, 100);
}
如果 message 在 setTimeout 执行前已经被归还池中并重新使用,那么定时器中读取到的可能是另一个消息的数据。
对于异步场景,应该复制数据,或者延迟归还:
javascript
function processMessageAsync(message) {
const snapshot = {
type: message.type,
id: message.id,
payload: message.payload
};
setTimeout(() => {
console.log(snapshot.type);
}, 100);
}
八、对象池的核心设计原则
1. 对象必须具有明确的生命周期
适合对象池的对象通常是:
rust
获取 -> 使用 -> 释放
例如:
- 粒子;
- 子弹;
- 临时向量;
- 渲染命令;
- 网络消息;
- 短生命周期任务对象。
不适合对象池的对象通常是:
- 生命周期不确定的业务实体;
- 会被多个模块长期持有的对象;
- 经常异步传递的对象;
- 状态复杂且难以完全重置的对象。
2. 获取对象后不能长期持有
对象池对象本质上是"借来的对象"。
ini
const item = pool.acquire();
// 使用 item
pool.release(item);
释放以后不能继续使用:
scss
pool.release(item);
// 错误:item 已经归还
console.log(item.name);
更严重的情况是:
ini
const cache = [];
const item = pool.acquire();
cache.push(item);
pool.release(item);
此时 cache 仍然保存着一个已经被归还的对象。对象池再次复用它后,cache 中的数据可能发生变化。
3. 清理引用,避免内存被意外保留
如果对象中保存了大型数组、DOM 节点或复杂数据,释放时应该清理引用:
ini
const taskPool = new ObjectPool(
() => ({
callback: null,
data: null
}),
task => {
task.callback = null;
task.data = null;
}
);
否则对象池本身会一直持有这些引用,反而造成内存占用。
4. 保持对象结构稳定
JavaScript 引擎会根据对象结构进行优化。对象池中的对象最好保持一致的属性结构:
yaml
function createParticle() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
active: false
};
}
尽量避免这样动态添加或删除属性:
ini
particle.extraData = someData;
delete particle.extraData;
更好的方式是固定属性:
yaml
function createParticle() {
return {
x: 0,
y: 0,
vx: 0,
vy: 0,
life: 0,
active: false,
extraData: null
};
}
固定对象结构通常有利于 JavaScript 引擎优化访问性能。
九、对象池并不一定总能提升性能
对象池虽然可以减少对象分配,但也会带来额外成本:
- 获取和释放逻辑;
- 对象状态重置;
- 空闲对象长期占用内存;
- 代码复杂度增加;
- 生命周期管理困难;
- 使用不当可能产生数据污染。
现代 JavaScript 引擎对短生命周期对象有专门优化。很多普通代码中的临时对象,会在新生代垃圾回收中快速回收,实际成本可能并不高。
例如下面的代码不一定需要对象池:
javascript
function calculate(x, y) {
return {
x: x * 2,
y: y * 2
};
}
如果它调用次数不多,或者调用不在动画、物理、渲染等高频路径上,直接创建对象可能更简单、更安全。
十、什么时候适合使用对象池
适合使用对象池的典型场景:
- 每帧创建大量对象;
- 对象生命周期很短;
- 对象结构固定;
- 对象创建和销毁次数非常高;
- 能够明确控制获取与释放;
- 性能分析显示 GC 是主要瓶颈;
- 应用运行在内存或 CPU 较弱的设备上。
常见领域包括:
- Canvas、WebGL 游戏;
- 粒子系统;
- 物理引擎;
- 实时数据可视化;
- 高频日志解析;
- WebSocket 消息处理;
- 音频处理;
- 动画系统;
- 大规模列表或任务调度。
十一、什么时候不应该使用对象池
以下情况通常不建议使用:
1. 对象创建频率不高
arduino
const config = {
theme: "dark",
language: "zh-CN"
};
这种对象没有必要放入对象池。
2. 对象生命周期复杂
对象在多个组件之间传递、缓存、异步使用时,很难准确释放。
3. 对象状态复杂
如果对象包含几十个字段,并且部分字段之间存在复杂依赖,重置过程容易遗漏状态。
4. 对象必须长期保存
例如用户信息、页面状态、业务实体等,这些对象不符合"短生命周期、重复使用"的特征。
5. 没有经过性能测试
不要因为"创建对象会触发 GC"就盲目使用对象池。应先通过性能工具确认问题。
十二、对象池与数组、TypedArray 的选择
如果数据结构非常固定,甚至可以不使用普通对象,而使用数组或 TypedArray。
例如粒子数据:
ini
const maxParticles = 1000;
const positionsX = new Float32Array(maxParticles);
const positionsY = new Float32Array(maxParticles);
const velocitiesX = new Float32Array(maxParticles);
const velocitiesY = new Float32Array(maxParticles);
const lifes = new Float32Array(maxParticles);
const active = new Uint8Array(maxParticles);
这种"结构数组"(Structure of Arrays,简称 SoA)具有以下特点:
- 不创建大量对象;
- 数据连续,缓存友好;
- 适合批量计算;
- 适合 WebGL 和物理计算;
- 内存布局更加稳定。
相比之下,普通对象数组属于"数组中的结构"(Array of Structures,简称 AoS):
yaml
const particles = [
{ x: 0, y: 0, life: 1 },
{ x: 10, y: 20, life: 0.5 }
];
在数据规模很大、计算非常频繁时,TypedArray 可能比对象池更适合。
十三、如何正确评估对象池是否有效
不要只根据理论判断,应通过实际测试。
可以重点观察:
performance.now()测量的执行时间;- Chrome DevTools 的 Performance 面板;
- Memory 面板中的堆变化;
- GC 事件是否频繁;
- 帧率是否稳定;
- 内存是否持续增长;
- 移动设备上的实际表现。
简单测试:
ini
function benchmark(name, fn, count = 1000000) {
const start = performance.now();
for (let i = 0; i < count; i++) {
fn(i);
}
const end = performance.now();
console.log(`${name}: ${(end - start).toFixed(2)}ms`);
}
测试普通创建:
ini
benchmark("普通对象", i => {
const item = {
x: i,
y: i * 2
};
item.x += 1;
});
测试对象池:
ini
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
item => {
item.x = 0;
item.y = 0;
},
100
);
benchmark("对象池", i => {
const item = pool.acquire();
item.x = i;
item.y = i * 2;
item.x += 1;
pool.release(item);
});
需要注意,简单基准测试容易受到 JIT 优化、浏览器状态和机器负载影响,最好进行多轮测试,并使用真实业务场景验证。
十四、一个更实用的对象池封装
可以增加使用状态检查,帮助发现重复释放或释放非法对象的问题:
ini
class SafeObjectPool {
constructor(factory, reset, initialSize = 0, maxSize = Infinity) {
this.factory = factory;
this.reset = reset;
this.maxSize = maxSize;
this.pool = new Set();
for (let i = 0; i < initialSize; i++) {
const object = factory();
object.__inPool = true;
this.pool.add(object);
}
}
acquire() {
const iterator = this.pool.values();
const object = iterator.next().value;
if (object) {
this.pool.delete(object);
object.__inPool = false;
return object;
}
const created = this.factory();
created.__inPool = false;
return created;
}
release(object) {
if (!object || object.__inPool) {
throw new Error("对象无效或已经被释放");
}
this.reset(object);
if (this.pool.size < this.maxSize) {
object.__inPool = true;
this.pool.add(object);
}
}
}
生产环境中不一定需要加入这些检查,但在开发阶段可以帮助发现对象生命周期错误。
不过更推荐不要把 __inPool 这类调试字段永久加入核心对象,因为它会改变对象结构。可以通过 WeakSet 记录状态,或者仅在开发环境开启检查。
十五、实际开发建议
建议一:优先减少不必要的对象创建
例如:
css
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
可以改成:
css
function addTo(out, a, b) {
out.x = a.x + b.x;
out.y = a.y + b.y;
}
这通常比引入完整对象池更简单。
建议二:在热点路径使用对象池
只在以下代码中考虑对象池:
- 每帧执行的代码;
- 大量循环;
- 物理和渲染计算;
- 高频网络消息;
- 粒子、子弹等重复出现的实体。
建议三:设置对象池上限
不要让对象池无限增长:
ini
const pool = new ObjectPool(
() => ({ x: 0, y: 0 }),
item => {
item.x = 0;
item.y = 0;
},
100,
1000
);
如果对象池没有上限,异常流量可能导致池长期持有大量对象。
建议四:不要释放仍然被使用的对象
对象池最严重的问题通常不是性能问题,而是数据错误。要明确:
markdown
谁获取,谁负责释放;
释放之后,不能继续使用;
异步任务不能直接持有池中对象。
总结
对象池的本质是:
markdown
通过复用对象,减少重复内存分配和垃圾回收压力
它非常适合:
- 粒子;
- 子弹;
- 敌人;
- 临时向量;
- 高频消息;
- 动画和物理计算对象。
但对象池并不是万能优化方案。现代 JavaScript 引擎对短生命周期对象已经进行了大量优化,在普通业务代码中,直接创建对象往往更简单、更可靠。
比较推荐的优化顺序是:
- 先确认是否存在性能问题;
- 使用 Performance 和 Memory 工具定位瓶颈;
- 优先减少不必要的对象创建;
- 尝试复用输出对象或使用固定数组;
- 在确实存在高频分配和 GC 压力时引入对象池;
- 明确对象生命周期,并完善重置、释放和容量限制;
- 对比优化前后的真实数据,而不是只看理论。
只有在"对象创建频繁、生命周期短、结构固定、生命周期可控"的场景下,对象池才真正具有较高的应用价值。