前端 JavaScript 原型链与对象继承
前端 JavaScript 原型链与对象继承
JavaScript 对象通过原型委托共享属性和方法。理解原型链有助于使用对象、类和继承,也能解释属性查找、方法覆盖以及 this 指向等常见问题。
一、属性查找沿原型链进行
读取对象属性时,JavaScript 先检查对象自身属性;找不到时再沿内部原型继续查找,直到原型为空。继承方法通常存放在原型上,而不是复制到每个实例。
const baseActions = {
describe() {
return "基础对象";
},
};
const item = Object.create(baseActions);
item.name = "条目";
item.describe();
对象自身的 name 与原型上的 describe 属于不同层级。属性遮蔽会让对象自身同名属性优先于原型属性。
二、使用类语法表达构造关系
class 是构建原型关系的语法形式。实例方法默认放在原型上,静态方法属于类本身。构造函数负责初始化实例字段。
class Record {
constructor(id) {
this.id = id;
}
key() {
return String(this.id);
}
}
类语法让构造和继承更易读,但并没有把 JavaScript 改成传统的类实例复制模型。
三、继承时保持职责清晰
extends 建立原型链关系,子类构造函数需要先调用 super 才能使用 this。子类应表达稳定的“是一种”关系。仅为了复用少量方法而建立深层继承树,往往会让改动互相牵连。
组合可以通过注入能力对象或纯函数共享行为,通常比多层继承更灵活。选择方式时看领域关系和变化方向,不只看代码行数。
四、注意 this 的调用方式
普通函数的 this 由调用方式决定。把对象方法单独取出后调用,this 可能不再指向原对象。箭头函数不创建自己的 this,适合捕获外层上下文,但不适合作为需要动态接收者的方法。
五、区分自有属性与继承属性
遍历对象和判断属性归属时,应区分对象自身属性与原型属性。处理不可信对象键时,使用明确的自有属性判断,避免意外读取原型链上的内容。
六、慎重修改内建原型
给内建原型增加属性可能与浏览器、依赖库或未来标准冲突,也会改变全局对象行为。扩展能力应放在独立模块或包装对象中,并避免污染全局原型。
实践中的边界
- 不可信键值对象应只读取自有属性。原型链上可能存在同名属性,通用合并逻辑若不设边界会产生意外行为。
- 方法从对象上解构出来单独调用时,再检查 this 是否仍有正确接收者。需要稳定函数引用时可显式绑定或改为闭包形式。
- 子类覆盖父类方法时保持调用契约一致,尤其是参数、返回类型和异常行为。过度依赖 super 链会让小改动牵动多层实现。
- 共享行为若需要组合多种能力,优先考虑独立函数或能力对象,而不是扩展越来越深的继承树。
- 不要修改浏览器内建原型来添加便利方法。局部包装和命名导出更容易测试,也不会与其他脚本发生全局冲突。
复核场景
设计业务对象时,把稳定共享行为放在类实例方法或独立模块,把每个对象的数据留在实例字段。若子类只是为覆盖一两个条件建立出来,改用参数或组合函数可能更直接。处理从接口解析的键值对象时,不假定其原型安全,先构造成应用自己的普通模型。
场景取舍补充
对象模型若主要由少量数据和行为组成,不必为了面向对象形式强制创建类层级。选择对象组合、函数或类时关注行为共享方式、实例生命周期和测试替换成本。
继承结构需要同时验证实例创建、方法覆盖和 this 调用方式;公共类接口变化时,检查所有子类是否仍满足原有行为约定。
若对象由多来源数据组合而成,在边界处构造成稳定的应用对象,避免把不可信原型和外部字段直接带入继承结构。
七、属性描述符和枚举行为
对象属性除了值,还包含可写、可枚举和可配置等属性描述信息。Object.keys 只返回可枚举的自有字符串键;Reflect.ownKeys 能覆盖更多自有键;for…in 还会遍历原型链上的可枚举属性。选择遍历方式前要明确目标是对象自己的字段还是全部可见字段。
只读和不可枚举属性可以表达对象契约,但广泛使用冻结或不可配置描述符会增加测试和扩展难度。公共数据模型通常更适合通过模块约定控制写入。
八、原型链和不可信对象
把用户提供的键合并到普通对象时,需要防止危险键影响原型或覆盖内部属性。对任意键值数据,可采用无原型对象、白名单字段或安全合并方法,并只读取自有属性。深拷贝也不自动解决原型污染,必须检查输入结构。
九、类继承的调用约束
子类构造前需要执行父类初始化,方法覆盖还应保持行为契约。静态方法属于类本身,不会像实例方法一样按同一方式在实例上查找。类的私有字段限制外部访问,适合封装内部不变量,但会让测试只能通过公开方法验证行为。
若继承树中多个子类互相覆盖调用,修改父类后很容易产生连锁影响。将可组合能力拆为纯函数或独立对象,通常更便于按需组合和替换。
十、对象模型的选择
简单数据记录优先使用普通对象;有明确生命周期和不变量时可以使用类;多种行为组合且关系并非严格“是一种”时更适合组合。设计完成后检查调试器中对象结构是否直观,序列化和测试是否符合预期。
实施中的取舍与复核
原型链决定属性查找和方法共享,但对象自身属性会遮蔽原型上的同名属性。使用继承前应确认“是一种”的关系真实稳定;仅为了复用几个方法而建立很长的继承链,会让行为依赖构造顺序和隐式属性查找。组合对象或委托函数往往更容易测试,也便于单独替换能力。
对象字面量、构造函数、类语法最终仍通过原型组织方法。类字段通常属于实例,原型方法由实例共享;把箭头函数作为实例字段会固定 this,但每个实例都会创建新的函数。选择时要结合实例数量、回调传递和方法可替换性,避免为了形式统一忽略内存与绑定行为。
原型污染风险来自把不可信键写入普通对象或递归合并对象。处理外部数据时应限制允许字段、拒绝特殊原型键,或使用适合字典语义的数据结构;权限判断也不要只依赖对象是否拥有某个可被注入的属性。序列化和反序列化后再验证结构,不能把普通 JSON 对象误认为可信领域实体。
调试属性来自何处时,分别检查对象自身属性和原型链,并关注属性描述符、可枚举性及 getter 副作用。测试继承关系时覆盖属性遮蔽、方法覆盖、静态成员和实例独立性。修改原型属于全局行为,运行时动态补丁通常难以追踪,优先通过局部包装器或显式组合完成扩展。
复核案例与后续维护
内建对象和业务对象的继承边界要谨慎。直接修改内建原型会影响页面上所有实例,容易与依赖库冲突;扩展能力应通过包装器、组合函数或子类在局部表达。检查原型时也要防止意外调用 getter,因为属性访问可能执行代码并产生副作用。
对象复制并不会自动保留原型、不可枚举属性和访问器语义。浅拷贝只复制一层引用,深拷贝也不适用于所有对象类型;克隆配置时先确定允许的数据形状。对外传入的对象可转换为明确的纯数据结构,减少调用方对原型链的隐式依赖。
权限和配置判断要使用可靠的自有属性检查,并对外部键做白名单验证。原型链上恰好存在同名属性可能造成逻辑误判。代码审查中关注动态合并和递归遍历入口,对相关工具增加特殊键测试,确保普通用户数据不会改变对象的继承行为。
总结
原型链让对象共享行为并委托属性查找。类语法改善表达,继承适合稳定的类型关系,组合适合灵活复用。理解 this 的调用规则、自有属性边界和原型污染风险,能避免隐藏的全局耦合。
- 点赞
- 收藏
- 关注作者
评论(0)