原型式继承在Ja vaScript开发者圈子里,算是一种既熟悉又容易被忽略的写法。很多人一提到继承,脑子里蹦出来的都是class或构造函数那套模式,但原型式继承其实提供了一种更轻量、更直接的委托机制——它不需要造出一个构造函数,直接拿一个现成的对象当模板,通过Object.create()就能快速创造新对象,同时让新对象“挂靠”到原对象的原型链上。换句话说,你得到的不是原对象的副本,而是一个能自动查找到原对象属性和方法的袋里。

这种写法的核心魅力在于:行为复用,而不是数据复制。你只需要定义一个基础对象,然后每次通过Object.create()生成新对象,新对象上的属性查找都会委托回原对象。一旦你对新对象添加自己的属性,它们就会覆盖原对象上的同名属性,但不会影响原对象和其他派生对象。
怎么写?
最简写法确实只有一行——不过理解背后的机制比背代码更重要。来看看这个经典操作:
const parent = { name: 'Base', skills: ['JS'] };
const child = Object.create(parent);
child.name = 'Derived'; // 自身属性
child.skills.push('React'); // 注意:引用类型仍共享
这里有几个关键点需要捋清楚:
- Object.create(obj) 创建了一个空对象,它的内部
[[Prototype]]直接指向obj。也就是说,child的原型是parent。 - 生成的对象没有任何构造函数调用,所以不会有自动绑定的
constructor属性——如果你需要,可以手动加上。 - 所有的属性读取都是“先查自己,自己没有再往上找原型”,这就是委托的本质。所以
child.name返回 'Derived'(自有的),而child.skills其实是从parent上拿到的同一个数组。
注意上面代码中 child.skills.push('React') 的行为:因为 skills 是数组(引用类型),操作的是原型对象上的同一个数组,所以所有派生对象共享这个数组。这不是bug,是委托机制的必然结果,后面会详细讲什么时候该用、什么时候得小心。
适用场景
原型式继承最适合那种“对象即配置”或“对象即模板”的轻量场景,不需要引入类或构造函数的抽象层级。下面几个场景在实际开发中很常见:
- 配置对象派生:假设你有一个基础配置对象,里面定义了API地址、超时时间、默认缓存策略等。用
Object.create(baseConfig)可以快速生成 dev、prod、test 等变体,每个变体只需覆盖自己需要的字段,其余自动继承。省去了写大量重复配置代码的麻烦。 - 游戏或实体系统中的轻量委托:比如一个游戏的基础实体
entity对象,它包含update()、render()等通用方法。然后你可以通过player = Object.create(entity)和enemy = Object.create(entity)分别创建玩家和敌人对象,各自添加专属的逻辑(比如玩家的键盘操控、敌人的AI状态机),而底层的移动、碰撞检测等行为全部委托给entity。这样做既避免了类继承的繁琐,又保持了代码复用。 - 避免深拷贝开销的临时扩展:有时候你只是想临时给某个对象挂上一组辅助方法,又不想污染原对象本身,也不想为了这个临时行为写一个完整的类。用
Object.create(templateWithMethods)生成一个新的对象,在这个新对象上操作,原对象纹丝不动。这个方法在数据处理、中间件扩展中很有用。 - 快速构建原型链实验或测试桩:在调试或者写单元测试时,想测试某个依赖原型链的特性,或者模拟一组对象之间的继承关系,直接用
Object.create构建原型链是最快的方式——不需要定义任何构造函数。
注意什么?
原型式继承不是万能药,它有明确的边界和坑,使用前心里要清楚:
- 引用类型共享:如前面演示的,数组、对象等引用类型值会直接共享——你在一处修改,所有派生对象都会感知到。如果你需要每个派生对象独立持有自己的引用数据,就不能用这种模式,或者需要在创建派生对象后主动复制一份引用数据。
- 不支持传参初始化:因为没有构造函数,所以无法像
new Parent(args)那样在创建对象时传入参数进行初始化。如果你需要根据参数动态生成不同的初始状态,原型式继承不是好选择。 - 不适合严格类型识别:通过
Object.create创建的对象是一个普通对象,不是某个构造函数的实例。所以instanceof检查会失效。如果你需要根据类型做不同处理(比如instanceof Player),还是得用 class 或构造函数。
总的来说,原型式继承是Ja vaScript原型链机制的直白体现。它用最简单的方式实现了“模板→派生”的委托关系,特别适合那些配置驱动、数据密度低、不需要严格类型隔离的场景。用对了地方,代码会变得干净利落;如果硬套到需要复杂初始化和严格类型的地方,反而会有麻烦。理解了这些边界,你就能在合适的项目中放心地用它了。
