JavaScript的继承体系和传统面向对象语言差别很大,理解原型链是写好可维护代码的前提。很多人以为用了class关键字就进入了经典继承世界,其实底层仍然是对象之间的委托关系。只有摸清这条链路的查找规则,才能在类设计模式和原型组合之间做出合理取舍。

原型链的基本运作原理
在JavaScript中,每当创建一个函数,引擎就会为它附加一个prototype属性,这是一个普通对象。当使用new调用该函数时,实例内部会生成一个不可见的[[Prototype]]引用,指向构造函数的prototype。读取实例属性时,若自身没有,就会顺着[[Prototype]]一直往上找,直到Object.prototype为止。
这种查找方式意味着所有实例共享原型上的方法和属性。下面的代码展示了最基本的原型链结构,以及属性遮蔽现象:
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
return '我是' + this.name;
};
function Dog(name) {
Animal.call(this, name);
}
// 关键:让Dog.prototype指向一个Animal实例,形成链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return this.name + '在叫';
};
var d = new Dog('阿黄');
console.log(d.say()); // 来自Animal.prototype
console.log(d.bark()); // 来自Dog.prototype
上面通过Object.create切断了默认的原型关联,避免了Dog.prototype直接引用Animal.prototype导致的互相污染。如果漏掉constructor修正,实例化对象的constructor会错误地指向Animal,在需要判断类型时引发误判。
原型链过长会带来调试困难。每一层委托都增加了属性解析的跳数,且在多重继承模拟中,父类方法可能意外覆盖。因此社区普遍建议原型链深度不要超过两到三层。
ES6 class是语法糖还是新模型
从ES6开始,class关键字让声明看起来更像Java或C#。但编译后,它依然是利用原型与构造函数运作。extends背后自动调用了Object.setPrototypeOf,并把super绑定到父类。
看一段class写法和它等价于原型写法的对照:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ',' + this.y + ')';
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return super.toString() + ':' + this.color;
}
}
这段代码在语义上和手写原型继承一致,但class提供了更严格的初始化顺序:子类构造函数必须先调用super,否则this不可用。这对新手更友好,也减少了忘记绑定原型的风险。
不过class并没有引入真正的私有状态,仅仅靠命名约定或最近的#私有字段来限制。如果业务模型频繁变动,固定的类层级反而会成了束缚,因为改父类常迫使所有子类同步修改。
类设计模式的常见陷阱
采用经典继承树时,开发者容易把“是一个”关系强行套用。比如Square继承自Rectangle,看似合理,但当Rectangle增加width和height独立setter时,Square就要重写大量逻辑来保持长宽相等,耦合迅速加剧。
用组合代替继承能缓解这个问题。下面用工厂函数把行为附加到对象上,而不是锁死在类树里:
function canEat(obj) {
obj.eat = function() { return '吃东西'; };
return obj;
}
function canRun(obj) {
obj.run = function() { return '跑起来'; };
return obj;
}
function createCreature() {
var o = {};
canEat(o);
canRun(o);
return o;
}
var c = createCreature();
console.log(c.eat());
console.log(c.run());
这种轻量组合没有原型查找开销,也不会因为继承层级变动而崩溃。对于UI组件、工具对象等场景,组合往往比abstract class更灵活。
当然,如果领域模型稳定且类型关系明确,例如各种形状都共用同一套几何算法,class仍然是不错的载体。选择依据是变化频率,而不是语言潮流。
性能与可维护性对比
从性能看,原型方法因为共享函数对象,内存占用低于每个实例都拷贝一份。class编译出的方法同样挂在原型,因此二者没有本质差别。真正影响维护的是结构清晰度。
| 维度 | 原型继承 | class继承 | 组合模式 |
|---|---|---|---|
| 语法直观度 | 低 | 高 | 中 |
| 耦合程度 | 中 | 高 | 低 |
| 动态扩展 | 易 | 较难 | 极易 |
| 调试难度 | 中 | 低 | 低 |
表里可以看出,class在直观度和调试上占优,但耦合最高。原型写法虽然灵活,却容易写出隐晦的链。组合在动态扩展上最好,适合需求摇摆的项目。
实际工程中,可以混合使用:用class定义稳定核心实体,用Object.assign把可选能力混入实例。这样既保留类型语义,又避开深层继承。
实践中的选型建议
先问自己两个问题:对象间是否是强“是一个”关系?类型结构半年内会变吗?如果都是肯定,用class。如果后者是否定,原型或组合更安全。
举一个表单校验的例子,不同字段校验规则差异大,却共享提交逻辑。此时把校验器作为函数组合进表单对象,比让每种字段都继承BaseField更省力:
function withRequired(field) {
field.validate = function(val) {
return val ? null : '必填';
};
return field;
}
function withNumber(field) {
var old = field.validate;
field.validate = function(val) {
var err = old ? old(val) : null;
if (err) return err;
return isNaN(val) ? '需为数字' : null;
};
return field;
}
var ageField = withNumber(withRequired({}));
console.log(ageField.validate('')); // 必填
console.log(ageField.validate('abc')); // 需为数字
console.log(ageField.validate('20')); // null
上述代码把校验能力分层叠加,没有产生任何父类子类。新增规则只需再包一层函数,原有逻辑不受影响。
总结来说,原型链是JavaScript的基石,class是其美化语法,组合是应对变化的缓冲层。弄清三者边界,才能在设计中收放自如。
JavaScriptprototype_chainclass_design_pattern修改时间:2026-08-06 00:48:32