修改原型prototype会有什么风险?

曾经看到过修改prototype这种做法不好,本人实际使用感觉非常方便,有什么不好的?
例如:
要在所有的react组件或vue组件中,做数字的格式化,直接如下:

String.prototype.toNum = Number.prototype.toNum = function(s) {
    
}

就能在所有组件中的任何字段,方便的调用toNum方法了,无需每个组件引入方法

阅读 3.4k
avatarAI BotBETA

在JavaScript中修改内置对象的原型(如String.prototypeNumber.prototype等)确实可能带来一些风险,尽管在某些场景下它看起来非常方便。以下是修改原型时可能遇到的一些风险和问题:

  1. 命名冲突:如果你添加的方法与现有方法或未来可能会添加的方法同名,那么你的方法将会覆盖它们,这可能导致难以调试的问题。
  2. 不可预测性:当你修改原型时,你改变了所有实例的行为。这可能会导致在不知情的情况下影响其他代码段。特别是在大型项目或第三方库中,这种改变可能是灾难性的。
  3. 非标准性:修改内置对象的原型并不是ECMAScript标准所推荐的做法。这意味着它可能不会被所有环境或未来的JavaScript版本所支持。
  4. 测试困难:如果你修改了原型,那么测试你的代码可能会变得更加困难,因为你需要确保所有的代码(包括你的代码和任何依赖的代码)都与你的修改兼容。
  5. 性能问题:虽然这通常不是主要问题,但在某些情况下,修改原型可能会引入一些小的性能开销,因为每次访问一个方法时都需要进行额外的检查。
  6. 可读性:对于不熟悉你的代码的人来说,看到你在使用someString.toNum()这样的方法可能会感到困惑,因为他们可能不知道toNum是哪里来的。
  7. 扩展性:如果你希望在未来扩展你的toNum方法(例如,添加新的参数或行为),那么你可能会发现很难在不破坏现有代码的情况下进行这样的扩展。

对于你的例子,即希望在所有的React或Vue组件中方便地格式化数字,更好的做法可能是创建一个工具函数或模块,并在需要的地方导入它。例如:

// numFormatter.js
export function formatNumber(num) {
    // 你的格式化代码
}

// 在你的组件中
import { formatNumber } from './numFormatter';

// 使用
const formattedNum = formatNumber(someNumber);

这种方法更加清晰、可预测,并且更容易测试和扩展。

8 个回答

知道为什么JS的String、Array的包含方法不像其他语言那样叫a.contains(),而是叫a.includes()吗?因为曾经有个大聪明(MooTools),在String.prototype上挂了个非标准的contains(),结果标准委员会只好改名为includes,避免破坏那些旧的contains()的行为,导致使用MooTools的网站崩溃。

知道为什么JS的groupBy长这个样子:

Object.groupBy(array, (num, index) => {
  return num % 2 === 0 ? 'even': 'odd';
})

而不是这个样子:

array.groupBy((num, index) => {
  return num % 2 === 0 ? 'even': 'odd';
})

因为当年有个大聪明(Sugar),在Array.prototype上挂了个非标准的groupBy(),结果标准委员会只好改为静态方法Object.groupBy,避免破坏那些旧的groupBy()的行为,导致使用Sugar的网站崩溃。

类似的还有Array.prototype.flatArray.prototype.flatten之争,不一一列举了。

标准委员会每当要给JS添加新功能时,都会考虑这些新功能会不会破坏当前环境的兼容性。如果影响较大,他们就会考虑做出让步。
但是,如果你没那么大影响力,他们就不会考虑对你的兼容性影响。你就要自己承担将来遇到breaking changes的风险了。

自己用当然感觉不错,但是如果需要跟人合作就很麻烦了。现代化软件开发几乎不可能一个人完成,都要大量使用第三方开源仓库。如果每个人都往原型上挂东西,每个人写法都不一样,A 跟 B 冲突,一单用了 A,全部跟 B 相关的都不能用,那就是一场灾难。

现实世界也是如此,每个看起来不太合理的规定背后,可能都是你没遇到过的惨痛经历。

不错的问题啊。其实问题本身前面基本都回答干净了,我就不罗嗦啦,换一个角度聊两句凑个热闹。

在我看来这是个哲学问题,从历史的角度来说,软件这个体系还很年轻,整个发展历程其实和人类史具有极大的相似性,例如函数式编程很像原始社会,面向对象有点像奴隶社会,微服务架构就相当于封建社会了,而去中心化之后就有点现代社会的感觉了。

而prototype来说,更像是早期东西大多不好用,那就改一改,用的顺手就好,后来就不满意了,我为什么要把A改成B(我把xxx优化了一下),我自己直接生产B不就好了(这是我做的),然后就产生了“轮子”问题,然后就催生了“不要重复做轮子”等观点。

所以这其实就是软件在开发思想上的迭代更新而已,正确看待就好。

再举个例子,现代人也会感叹某些老物件比现代的还精巧好用,其实高级语言为主的开发中不也偶尔混入C,汇编等语言提升效率么。

我非常喜欢这样的写法,面向对象的高级用法,逻辑上很实用

其实大部分的JavaScript库都在大量的使用。比如Vue
平常自己用也很好。非要说风险,那就是多人合作时,需要注意一些风险点。

考虑兼容性问题

不要去覆盖已有的函数,自定义的尽量把名字取的奇怪一点,避免未知的情况下覆盖了已有的函数,可以考虑加一些前缀。

// 假设这是一个已有的库代码
function doSomethingWithArray(arr) {
  for (let item of arr) {
    // 基于原始的 Array.prototype.includes 方法进行操作
    if (!arr.includes(item * 2)) {
      // 执行一些操作
    }
  }
}

// 其他地方修改了 Array.prototype.includes 方法
Array.prototype.includes = function (value, fromIndex) {
  // 自定义的实现,与原始行为不同
  return false;
};

let myArray = [1, 2, 3];
doSomethingWithArray(myArray); 
// 由于原型修改,这里的行为可能不再符合预期

代码可读性问题

别人阅读你代码时候,很陌生的函数''.toNum(),会疑惑干什么用的。尽可能可能把注释写写全问题也不大。告诉人家这个是哪儿定义的做什么用的。

性能影响

某些情况下,会影响JavaScript引擎优化策略,导致性能下降。特别是添加prototype特别多或者数据量特别大的情况下。

最好尽量是别在原型上挂载方法之类的,因为实际开发中会引用第三方的库,你不知道在原型上挂载方法之后会产生什么问题。

总结一下:

  1. 在Object.prototype上扩展方法,会导致for...in出错,必须加判断,以及第三方库莫名报错,完全不可
  2. 在原型上扩展方法,可能会与第三方库同名冲突,或者与团队其他人的方法同名冲突
  3. 可能会与新的ES标准方法冲突

在避开以上问题的情况下,适量使用原型扩展,还是很香的

性能问题:
修改prototype可能会影响性能。当访问一个对象的属性时,JavaScript会沿着原型链查找。频繁地修改prototype可能会导致缓存失效,从而影响性能。
调试困难:
修改prototype可能会使追踪错误变得复杂,因为问题可能源于任意一个使用该构造函数创建的对象。
原型污染:
攻击者可能利用能够修改对象原型的漏洞来注入恶意代码或者篡改对象的行为。如果允许外部输入影响prototype,则可能会影响到所有从该构造函数实例化的对象,甚至可能影响到全局的Object.prototype,从而影响整个应用中的所有对象。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏