无损克隆需借用构造函数配合原型链:先用Object.create()建立原型链,再用call重放初始化逻辑,确保新实例拥有独立状态和完整行为。仅用Object.create()会导致共享引用属性、缺失实例属性及无法执行构造逻辑。

借用构造函数配合原型链实现“无损克隆”,核心目标是:既保留原实例的动态行为(如方法调用、闭包状态、私有数据),又确保新实例完全独立,不与原实例共享可变状态。这不是单纯复制属性,而是重建实例身份的同时继承原始行为契约。
为什么不能只用 Object.create()?
Object.create() 只建立原型链继承,新对象自身为空,所有方法和数据都来自原型——这意味着:
- 方法虽可调用,但若方法内部依赖 this 上的实例属性(如
this.cache = new Map()),而该属性未在新对象上初始化,就会出错或共享; - 原型上的引用类型(如数组、对象)被所有继承者共用,修改一处影响全局;
- 无法还原构造时的初始化逻辑(如计数器自增、事件监听绑定、异步资源预热等)。
借用构造函数:重放初始化过程
让目标构造函数在新对象上下文中执行一次,相当于“重跑一遍 new 的逻辑”。关键在于用 call 或 apply 将 this 绑定到新对象上:
- 先用
Object.create(OriginalConstructor.prototype)创建一个空对象,并正确设置其 __proto__ 指向原构造函数的原型; - 再调用
OriginalConstructor.call(newObj, ...args),把构造函数里对 this 的赋值、方法绑定、状态初始化全部执行一遍; - 这样得到的新对象,既有原型链继承的方法,又有自己独占的实例属性(包括闭包捕获的变量、独立的 Map/Array 实例等)。
完整示例:克隆一个带内部缓存和计数器的 Service 实例
假设有一个类封装了请求缓存与调用统计:
class DataService {
constructor(baseURL) {
this.baseURL = baseURL;
this.cache = new Map(); // 引用类型,必须独立
this.callCount = 0; // 基本类型,也需隔离
this.id = Symbol('instance'); // 私有标识
}
fetch(key) {
this.callCount++;
if (this.cache.has(key)) return this.cache.get(key);
const data = `${this.baseURL}/${key}`;
this.cache.set(key, data);
return data;
}
}
无损克隆它:
function cloneInstance(instance) {
const Constructor = instance.constructor;
const clone = Object.create(Constructor.prototype);
Constructor.call(clone, instance.baseURL); // 重放初始化,传入必要参数
return clone;
}
const original = new DataService('https://api.example.com');
original.fetch('user');
console.log(original.callCount); // 1
const cloned = cloneInstance(original);
cloned.fetch('post');
console.log(cloned.callCount); // 1 ← 独立计数
console.log(original.callCount); // 1 ← 不受影响
console.log(cloned.cache === original.cache); // false ← 缓存分离
处理不可枚举、Symbol 属性与私有字段
ES6+ 的 #privateField 和 Symbol 键不会被 for...in 或 Object.keys 遍历到,但借用构造函数能自然覆盖它们——只要构造函数本身定义了这些字段,call 执行时就会重新创建。
- 私有字段(#xxx):构造函数内声明即生效,克隆后自动拥有新副本;
- Symbol 属性:如果构造函数中通过
this[Symbol.iterator] = ...设置,也会被重新赋值; - 不可枚举属性:只要不是手动用
Object.defineProperty设为enumerable: false,通常不影响;若需精确复制,可在 call 后补充遍历Object.getOwnPropertyNames(instance)和Object.getOwnPropertySymbols(instance)。
何时需要额外补丁?
如果原实例存在外部强引用(如定时器、事件监听器、DOM 绑定),仅靠构造函数重放无法自动解绑/重绑。这时需:
- 在构造函数中设计可复位的生命周期方法(如
init()、destroy()); - 或在克隆后手动调用清理逻辑,再触发重建;
- 避免在构造函数外直接操作全局状态,保持构造函数的幂等性。

















