根本原因是原型链共享引用类型:所有实例指向同一prototype内存地址,导致修改一个实例影响其他实例;解决方法是在构造函数或类字段中初始化引用属性,或用getter返回新对象。

根本原因不是代码写错了,是原型链机制在按规则运行:所有实例共享同一个 prototype 对象,而引用类型(数组、对象、Map 等)赋值时只复制内存地址——挂到 prototype 上,就等于让所有实例指向同一块内存。
为什么改一个实例,另一个也跟着变?
因为它们读的不是“自己的副本”,而是 prototype 上那个唯一的引用值。比如:
function Animal() {}
Animal.prototype.tags = [];
const dog = new Animal();
const cat = new Animal();
dog.tags.push('loyal');
console.log(cat.tags); // ['loyal'] ← 竟然也变了
这里 dog.tags 和 cat.tags 查找路径都是:自己没有 → 去 Animal.prototype 找 → 找到同一个 []。push 操作直接改了那块内存,自然全员可见。
子类继承时更容易中招
当用 Child.prototype = new Parent() 做原型链继承,父类构造函数执行一次,返回的对象成为子类原型——它身上的引用属性(如 this.items = [])就被固化在原型上,所有 new Child() 都共享。
立即学习“Java免费学习笔记(深入)”;
- 父类实例属性变成子类的原型属性
- 子类之间不隔离,也不和父类实例隔离
- 哪怕父类本身没被 new,只要它的 prototype 或构造结果含引用值,就会传染
怎么一眼确认是否已踩坑?
两行代码就能验证:
-
改一个,查另一个:
a.data.push(1); console.log(b.data)—— 如果 b 也多出元素,说明共享 -
查地址是否相同:
Object.getPrototypeOf(a).data === Object.getPrototypeOf(b).data返回true,就是同一份内存
真正有效的隔离方案
目标很明确:让每个实例拿到自己的引用副本,而不是共用“模板”。可靠做法只有三种:
-
构造函数内初始化:每次
new都执行一遍,天然隔离。function User() { this.list = []; this.config = {}; } -
类字段语法(推荐):ES6+ 写法,语义清晰,自动绑定到实例。
class Button { handlers = []; settings = { size: 'm' }; } -
getter 返回新对象:适合默认配置或懒加载。
get defaults() { return { timeout: 3000 }; }—— 每次访问都生成新引用,彻底隔绝副作用
子类继承时,必须在子构造函数里调用 Parent.call(this),把父类初始化逻辑执行在子实例上;同时用 Object.create(Parent.prototype) 设置原型,才能兼顾方法复用与数据隔离。


















