
本文详解如何正确继承 Decimal.js 的 Decimal 类,在保留全部原生方法的同时,确保所有内部构造调用(如 add()、plus() 等)均使用自定义的、具备输入校验能力的安全构造函数,彻底解决因 constructor 被硬编码覆盖导致的 sanitizer 失效问题。
本文详解如何正确继承 decimal.js 的 `decimal` 类,在保留全部原生方法的同时,确保所有内部构造调用(如 `add()`、`plus()` 等)均使用自定义的、具备输入校验能力的安全构造函数,彻底解决因 `constructor` 被硬编码覆盖导致的 sanitizer 失效问题。
JavaScript 中继承第三方类(尤其是像 decimal.js 这样高度定制化的库)常面临一个隐蔽却关键的问题:方法内部通过 new this.constructor(...) 创建新实例时,并不自动沿用子类逻辑。decimal.js 为兼容性与类型一致性,在每个实例创建后显式重置 this.constructor = Decimal(见源码约第4290行),这导致即使你正确扩展了 SafeDecimal 类,其 add()、dividedBy() 等方法仍会调用原始 Decimal 构造器——绕过你的输入校验逻辑,引发不可预期的错误(如 DecimalError)。
✅ 正确继承方案:手动修复 constructor 引用
核心解决方案非常简洁:在子类构造函数中,显式将 this.constructor 指向自身类,覆盖 decimal.js 的强制赋值:
import { Decimal } from 'decimal.js';
class SafeDecimal extends Decimal {
constructor(value: Decimal.Value) {
// ✅ 安全输入预处理:拒绝非法字符串(避免 parseFloat 的宽松解析)
if (typeof value === 'string') {
if (!/^-?\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(value.trim())) {
super('NaN'); // 或 throw new Error('Invalid numeric string');
return;
}
super(value); // 直接传入合法字符串 → decimal.js 内部精准解析
} else {
super(value);
}
// ? 关键修复:恢复 constructor 指向,确保方法内 new this.constructor() 使用 SafeDecimal
this.constructor = SafeDecimal;
}
}? 为什么必须加 this.constructor = SafeDecimal?
decimal.js 的 methodUsingConstructor(如 add, plus, times)内部均形如:const other = new this.constructor(otherValue);若不修复,this.constructor 始终为 Decimal,导致 otherValue 跳过你的校验逻辑,直接进入原始 Decimal 构造流程,破坏安全性设计。
?️ 增强校验:比 parseFloat 更可靠的字符串验证
parseFloat('123abc') === 123 是典型陷阱。推荐采用严格正则或 Number.isFinite(Number(str)) 结合白名单校验:
private static isValidNumericString(str: string): boolean {
const trimmed = str.trim();
// 允许:整数、小数、科学计数法(含正负号)
return /^-?\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(trimmed) &&
Number.isFinite(Number(trimmed));
}
// 在构造函数中使用:
if (typeof value === 'string') {
if (!SafeDecimal.isValidNumericString(value)) {
throw new Error(`Invalid numeric string: "${value}"`);
}
super(value); // ✅ 利用 decimal.js 原生字符串解析(无精度损失)
}? 验证效果:测试用例确保 sanitizer 生效
describe('SafeDecimal', () => {
it('sanitizes input in constructor', () => {
expect(new SafeDecimal('123').toString()).toBe('123');
expect(() => new SafeDecimal('abc')).toThrow('Invalid numeric string');
});
it('sanitizes input in chained methods', () => {
const safe = new SafeDecimal('10');
// ✅ 此处 add('invalid') 将触发 SafeDecimal 构造器校验,抛出预期错误
expect(() => safe.add('xyz')).toThrow('Invalid numeric string');
});
});⚠️ 注意事项与最佳实践
- 勿滥用 parseFloat 或 Number():它们会静默转换(如 '1e2' → 100, ' 42 ' → 42),但无法识别 '123abc' 中的非法后缀,应优先依赖 decimal.js 自身的字符串解析能力(new Decimal('123.45'))。
- 避免修改全局 Decimal:始终通过子类封装,保持原库纯净,便于升级与维护。
- 生产环境建议 fork + 提交 PR:若需长期维护,可考虑向 decimal.js 社区提议修复 constructor 覆盖逻辑(例如改为 Object.defineProperty(this, 'constructor', {...}) 并设为 writable: false),从根本上解决问题。
- TypeScript 类型兼容性:SafeDecimal 继承 Decimal 后,所有方法签名与返回类型自动继承,无需额外声明,类型安全由 decimal.d.ts 保障。
通过这一模式,你不仅获得了一个具备输入防护能力的高精度数值类,更掌握了在 JavaScript 生态中安全继承“反常规”第三方类的核心技巧——主动接管 constructor 引用,是绕过底层硬编码限制的最直接、最可靠路径。


















