为什么扩展本机对象是一种不好的做法?

IT技术 javascript prototype prototypal-inheritance
2020-12-21 16:40:28

每个 JS 意见领袖都说扩展原生对象是一种不好的做法。但为什么?我们的性能受到了影响吗?他们是否害怕有人“以错误的方式”做这件事,并向 中添加可枚举类型Object,实际上会破坏任何对象上的所有循环?

TJ Holowaychukshould.js为例。增加了一个简单的getterObject,一切工作正常(来源)。

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

这确实有道理。例如,可以扩展Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

是否有任何反对扩展本机类型的论据?

6个回答

当你扩展一个对象时,你改变了它的行为。

更改仅由您自己的代码使用的对象的行为是可以的。但是,当您更改其他代码也使用的某些内容的行为时,您可能会破坏其他代码。

当在 javascript 中向对象和数组类添加方法时,由于 javascript 的工作方式,破坏某些东西的风险非常高。多年的经验告诉我,这种东西会导致 javascript 中出现各种可怕的错误。

如果您需要自定义行为,最好定义您自己的类(可能是一个子类)而不是更改本机类。这样你就不会破坏任何东西。

改变一个类的工作方式而不对其进行子类化的能力是任何优秀编程语言的一个重要特性,但它必须很少使用并且必须谨慎使用。

有很多问题,例如,如果其他一些代码也尝试添加stringify()具有不同行为的自己的方法,会发生什么这真的不是你在日常编程中应该做的事情......如果你想做的只是在这里和那里保存一些代码字符,那就不是了。最好定义自己的类或函数来接受任何输入并将其字符串化。大多数浏览器都定义了JSON.stringify(),最好检查它是否存在,如果不存在,则自己定义它。
2021-02-08 16:40:28
我喜欢someError.stringify()errors.stringify(someError)它很简单,非常适合 js 的概念。我正在做一些专门绑定到某个 ErrorObject 的事情。
2021-02-11 16:40:28
如果问题是您可能发生碰撞,您是否可以使用一种模式,每次执行此操作时,您总是在定义函数之前确认该函数未定义?如果您总是将第三方库放在您自己的代码之前,您应该能够始终检测到此类冲突。
2021-02-12 16:40:28
剩下的唯一(但很好)的论点是,一些庞大的宏库可能会开始接管您的类型并可能相互干扰。或者还有别的吗?
2021-02-16 16:40:28
所以添加类似的东西.stgringify()会被认为是安全的吗?
2021-02-27 16:40:28

没有可衡量的缺点,例如性能下降。至少没有人提到任何。所以这是个人喜好和经验的问题。

专业人士的主要论点:它看起来更好,更直观:语法糖。它是一个特定于类型/实例的函数,因此它应该专门绑定到该类型/实例。

主要的反对意见:代码可以干扰。如果lib A 添加了一个函数,它可能会覆盖lib B 的函数。这可以很容易地破坏代码。

两者都有一个道理。当您依赖两个直接更改您的类型的库时,您很可能最终会得到损坏的代码,因为预期的功能可能不一样。我完全同意这一点。宏库不得操作本机类型。否则,作为开发人员,您将永远不知道幕后到底发生了什么。

这就是我不喜欢 jQuery、下划线等库的原因。别误会我的意思;它们的程序设计得非常好,工作起来很迷人,但它们很大你只使用了其中的 10%,理解了大约 1%。

这就是为什么我更喜欢原子方法的原因,在这种方法中,您只需要真正需要的东西。这样,你总是知道会发生什么。微型图书馆只做你想让他们做的事,所以他们不会干涉。在让最终用户知道添加了哪些功能的情况下,扩展本机类型可以被认为是安全的。

TL;DR如有疑问,请不要扩展本机类型。仅当您 100% 确定最终用户将了解并希望该行为时才扩展本机类型。任何情况下都不要操作本机类型的现有函数,因为它会破坏现有接口。

如果您决定扩展类型,请使用Object.defineProperty(obj, prop, desc); 如果不能,请使用类型的prototype.


我最初提出这个问题是因为我希望Errors 可以通过 JSON 发送。所以,我需要一种方法来对它们进行字符串化。error.stringify()感觉比errorlib.stringify(error); 正如第二个结构所暗示的那样,我是在操作errorlib而不是在操作error本身。

您还可以(从 es2015 开始)创建一个扩展现有类的新类,并在您自己的代码中使用它。所以如果MyError extends Error它可以有一个stringify方法而不与其他子类发生冲突。您仍然必须处理不是由您自己的代码生成的错误,因此它可能不如Error其他代码有用
2021-02-08 16:40:28
您是在暗示 jQuery 和下划线扩展本机对象吗?他们没有。所以如果你因为这个原因而避开它们,那你就错了。
2021-02-12 16:40:28
我对此持开放态度。
2021-02-19 16:40:28
@micahblu - 一个库可以决定修改一个标准对象只是为了方便它自己的内部编程(这通常是人们想要这样做的原因)。因此,不会仅仅因为您不会使用具有相同功能的两个库而避免这种冲突。
2021-02-25 16:40:28
这是我认为在使用 lib A 扩展本机对象可能与 lib B 冲突的论点中遗漏的一个问题:为什么您有两个在性质上如此相似或性质如此广泛以至于可能发生这种冲突的库?即我要么选择 lodash 要么选择下划线,而不是两者都选择。我们目前在 JavaScript 中的图书馆景观过于饱和,开发人员(通常)在添加它们时变得如此粗心,以至于我们最终避免了安抚我们的自由主义者的最佳实践
2021-03-04 16:40:28

在我看来,这是一种不好的做法。主要原因是整合。引用 should.js 文档:

OMG IT EXTENDS OBJECT???!?!@ 是的,是的,应该有一个 getter,不,它不会破坏你的代码

好吧,作者怎么知道?如果我的模拟框架也这样做呢?如果我的Promise库也这样做呢?

如果你在自己的项目中这样做,那就没问题了。但是对于图书馆来说,这是一个糟糕的设计。Underscore.js 是一个以正确方式完成事情的例子:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
我确定 TJ 的回应是不使用这些Promise或模拟框架:x
2021-02-17 16:40:28
这个_(arr).flatten()例子实际上说服了我不要扩展原生对象。我这样做的个人原因纯粹是语法上的。但这满足了我的美感 :) 即使使用一些更常规的函数名称,例如foo(native).coolStuff()将其转换为一些“扩展”对象,在语法上看起来也很棒。所以谢谢你!
2021-03-03 16:40:28

如果您逐个查看它,也许某些实现是可以接受的。

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

覆盖已经创建的方法会产生比它解决的更多的问题,这就是为什么在许多编程语言中通常声明要避免这种做法。开发人员如何知道功能已更改?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

在这种情况下,我们不会覆盖任何已知的核心 JS 方法,而是扩展 String。这篇文章中的一个论点提到新开发人员如何知道此方法是否是核心 JS 的一部分,或者在哪里可以找到文档?如果核心 JS String 对象获得一个名为capitalize的方法会发生什么

如果不是添加可能与其他库冲突的名称,而是使用所有开发人员都可以理解的公司/应用程序特定修饰符,该怎么办?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

如果您继续扩展其他对象,所有开发人员都会在野外遇到它们(在这种情况下)we,这会通知他们这是公司/应用程序特定的扩展。

这不会消除名称冲突,但确实减少了可能性。如果您确定扩展核心 JS 对象适合您和/或您的团队,那么这可能适合您。

扩展内置函数的原型确实是一个坏主意。然而,ES2015 引入了一种新技术,可用于获得所需的行为:

利用WeakMaps 将类型与内置原型相关联

以下实现扩展了NumberArray原型,而根本不接触它们:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

如您所见,即使是原始原型也可以通过这种技术进行扩展。它使用映射结构和Object身份将类型与内置原型相关联。

我的示例启用了一个reduce仅期望 anArray作为其单个参数的函数,因为它可以提取信息如何创建一个空的累加器以及如何从数组本身的元素中将元素与此累加器连接起来。

请注意,我可以使用普通Map类型,因为当弱引用仅表示从未被垃圾收集的内置原型时,它们没有意义。但是, aWeakMap不是可迭代的,除非您拥有正确的密钥,否则无法对其进行检查。这是一个理想的功能,因为我想避免任何形式的类型反射。

-1; 这有什么意义?您只是创建一个单独的全局字典,将类型映射到方法,然后按类型在该字典中显式查找方法。因此,您将失去对继承的支持(Object.prototype无法在 上调用以这种方式“添加”到的方法Array),并且与真正扩展原型的情况相比,语法明显更长/更丑陋。使用静态方法创建实用程序类几乎总是更简单;这种方法的唯一优点是对多态性的支持有限。
2021-02-11 16:40:28
这是 WeakMap 的一个很酷的用法。xs[0]在空数组上要小心type(undefined).monoid ...
2021-02-21 16:40:28
对此@ftor 的支持标准如何?
2021-02-24 16:40:28
@naomik 我知道 - 请参阅我的最新问题第二个代码片段。
2021-02-25 16:40:28
@MarkAmery 嘿伙计,你不明白。我不会失去继承,而是摆脱它。继承是80年代的,你应该忘记它。这个 hack 的重点是模仿类型类,简单地说,就是重载函数。不过,有一个合理的批评:在 Javascript 中模仿类型类有用吗?不,不是。而是使用本机支持它们的类型化语言。我很确定这就是你的意思。
2021-03-01 16:40:28