なぜネイティブオブジェクトの拡張が悪い習慣なのですか?


136

すべてのJSオピニオンリーダーは、ネイティブオブジェクトを拡張することは悪い習慣だと言っています。しかし、なぜ?パフォーマンスに影響はありますか?彼らは誰かがそれを「間違った方法」で行うことを恐れて、列挙可能な型をObjectに追加し、実際に任意のオブジェクトのすべてのループを破壊しますか?

テイクTJ Holowaychukshould.jsを例えば。彼はシンプルなゲッター追加Object、すべてが正常に動作します(ソース)。

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);

ネイティブ型の拡張に反対する議論はありますか?


9
後でネイティブオブジェクトが変更され、独自のセマンティクスを持つ「削除」関数が含まれるようになるとどうなりますか?あなたは標準を制御しません。
ta.speot.is 2012

1
それはあなたのネイティブタイプではありません。それはみんなのネイティブタイプです。

6
「誰かがそれを「間違った方法」で行うことを恐れて、列挙可能な型をObjectに追加し、実際に任意のオブジェクトのすべてのループを破壊しますか?」:うん。この意見が形成された当時、列挙できないプロパティを作成することは不可能でした。さて、この点で状況は異なるかもしれませんが、すべてのライブラリがネイティブオブジェクトを拡張するだけで、どのようにしたいかを想像してみてください。名前空間を使い始めたのには理由があります。
Felix Kling

5
たとえば、いくつかの「意見リーダー」に値するものについては、Brendan Eichは、ネイティブプロトタイプを拡張することは完全に良いと思います。
Benjamin Gruenbaum 2014

2
fooは、これらの日グローバルすべきではない、我々は、requirejs、commonjsなどが含まれている
ジェイミー・パテ

回答:


127

オブジェクトを拡張すると、その動作が変わります。

独自のコードでのみ使用されるオブジェクトの動作を変更しても問題ありません。ただし、他のコードでも使用されているものの動作を変更すると、他のコードが破損する危険があります。

JavaScriptでオブジェクトと配列クラスにメソッドを追加することになると、JavaScriptのしくみにより、何かを壊すリスクが非常に高くなります。長年の経験から、この種のものはJavaScriptにあらゆる種類のひどいバグを引き起こすことがわかりました。

カスタム動作が必要な場合は、ネイティブクラスを変更するのではなく、独自のクラス(おそらくサブクラス)を定義する方がはるかに優れています。そうすれば、何も壊さなくなります。

クラスをサブクラス化せずに動作を変更する機能は、優れたプログラミング言語の重要な機能ですが、まれに、注意して使用する必要があります。


2
のようなものを追加する.stgringify()と安全と見なされますか?
buschtoens 2012

7
たくさんの問題があります。たとえば、他のコードstringify()が異なる動作を持つ独自のメソッドを追加しようとするとどうなりますか?これは、日常のプログラミングでやるべきことではありません...ほんの数文字のコードをあちこちに保存するだけの場合はそうではありません。任意の入力を受け入れてそれを文字列化する独自のクラスまたは関数を定義することをお勧めします。ほとんどのブラウザはを定義していますがJSON.stringify()、存在するかどうかを確認し、存在しない場合は自分で定義することをお勧めします。
Abhi Beckert

5
私が好むsomeError.stringify()オーバーerrors.stringify(someError)。それは簡単で、jsの概念に完全に適合します。特定のErrorObjectに具体的にバインドされていることをしています。
buschtoens 2012

1
残っている唯一の(しかし良い)議論は、いくつかの巨大なマクロライブラリがあなたのタイプを引き継ぎ始め、お互いに干渉するかもしれないということです。それとも他に何かありますか?
buschtoens

4
問題が衝突の可能性がある場合、これを行うたびに、定義する前に関数が未定義であることを常に確認するパターンを使用できますか?そして、サードパーティのライブラリを常に自分のコードの前に置くと、そのような衝突を常に検出できるはずです。
Dave Cousineau 2016

32

パフォーマンスへの影響など、測定可能な欠点はありません。少なくとも誰も言及しなかった。したがって、これは個人の好みと経験の問題です。

主なプロの議論:見た目が良く、より直感的です:構文糖。これはタイプ/インスタンス固有の関数であるため、そのタイプ/インスタンスに具体的にバインドする必要があります。

主な反対論:コードは干渉する可能性があります。lib Aが関数を追加すると、lib Bの関数が上書きされる可能性があります。これにより、コードが非常に簡単に破損する可能性があります。

どちらにもポイントがあります。タイプを直接変更する2つのライブラリに依存している場合、期待される機能がおそらく同じではないため、コードが壊れる可能性が高くなります。私はそれに完全に同意します。マクロライブラリはネイティブタイプを操作してはなりません。そうでなければ、開発者としてあなたは裏で本当に何が起こっているのかを知ることができません。

そしてそれが、jQueryやアンダースコアなどのライブラリが嫌いな理由です。誤解しないでください。彼らは完全にうまくプログラムされていて、魅力のように機能しますが、それらは大きいです。それらの10%だけを使用し、約1%を理解します。

それが私が原子論的アプローチを好む理由です、本当に必要なものだけを必要とます。このように、あなたは常に何が起こるかを知っています。マイクロライブラリはあなたがやりたいことだけをするので、干渉しません。どの機能が追加されるかをエンドユーザーに認識させるという状況では、ネイティブタイプの拡張は安全であると考えることができます。

TL; DR疑わしい場合は、ネイティブ型を拡張しないでください。エンドユーザーがその動作を知っており、その動作を望んでいることを100%確信している場合にのみ、ネイティブ型を拡張してください。ではありません、それは既存のインタフェースを破るように、ケース、ネイティブ型の既存の機能を操作します。

タイプを拡張する場合は、Object.defineProperty(obj, prop, desc);を使用します。できない場合は、型のを使用してくださいprototype


ErrorsをJSON経由で送信可能にしたかったので、私は最初にこの質問を思いつきました。したがって、それらを文字列化する方法が必要でした。error.stringify()よりもずっと良い感じerrorlib.stringify(error)でした。2番目の構成が示唆するように、私はそれ自体errorlibではなく操作していerrorます。


私はこれについてさらに意見を述べたいと思います。
buschtoens

4
jQueryとアンダースコアがネイティブオブジェクトを拡張することを意味していますか?彼らはしません。したがって、その理由でそれらを回避している場合は、間違いです。
JLRishe

1
ここでは、ネイティブオブジェクトをlib Aで拡張するとlib Bと競合する可能性があるという議論で見逃されていると思う質問があります。この2つのライブラリが性質が非常に似ているか、性質が非常に広いため、この競合が発生する可能性があるのはなぜですか。つまり、ロダッシュかアンダースコアのどちらかを選択します。JavaScriptでの現在のlibrarayの状況は非常に飽和しており、開発者(一般的に)はそれらを追加する際に不注意になり、Lib Lordをなだめるためのベストプラクティスを回避することになります
micahblu

2
@micahblu-ライブラリは、独自の内部プログラミングの便宜のために標準オブジェクトを変更することを決定できます(そのため、多くの場合、人々はこれを行います)。したがって、同じ機能を持つ2つのライブラリを使用しないという理由だけで、この競合を回避することはできません。
jfriend00 2015

1
(es2015の時点で)既存のクラスを拡張する新しいクラスを作成し、独自のコードで使用することもできます。そのため、他のサブクラスと衝突することなくメソッドをMyError extends Error持つことができる場合stringify。自分のコードでは生成されないエラーを処理する必要があるため、他のコードErrorよりも有用ではない場合があります。
ShadSterling 2018年

16

私の意見では、それは悪い習慣です。主な理由は統合です。should.jsドキュメントの引用:

OMG IT EXTENDS OBJECT ???!?!@はい、そうです。1つのゲッターを使用する必要があります。コードを壊すことはありません。

さて、著者はどのように知ることができますか?私のモックフレームワークが同じことをしたらどうなりますか?私のpromise libが同じことをしたらどうなりますか?

あなた自身のプロジェクトでそれをしているなら、それは問題ありません。しかし、ライブラリの場合、それは悪いデザインです。Underscore.jsは、正しい方法で行われた処理の例です。

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()

2
TJの対応は、これらの約束やモックフレームワークを使用しないことだと確信しています:x
Jim Schubert

この_(arr).flatten()例では、ネイティブオブジェクトを拡張しないことを実際に確信しました。そうするための私の個人的な理由は、純粋に構文上のものでした。しかし、これは私の美的感覚を満たします:)それをいくつfoo(native).coolStuff()かの「拡張」オブジェクトに変換するような、より規則的な関数名を使用しても、構文的に見栄えがします。どうもありがとう!
たとえば、

16

ケースバイケースでそれを見ると、おそらくいくつかの実装は受け入れられます。

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を拡張します。この投稿の1つの議論は、新しい開発者がこのメソッドがコア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オブジェクトの拡張があなたやあなたのチームのためであると判断した場合、おそらくこれはあなたのためです。


7

組み込みのプロトタイプを拡張することは確かに悪い考えです。ただし、ES2015は、望ましい動作を得るために利用できる新しい手法を導入しました。

活用 WeakMap組み込みのプロトタイプを関連付けるタイプに複数可

次の実装では、NumberおよびArrayプロトタイプをまったく変更せずに拡張しています。

// 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 IDをタイプを組み込みプロトタイプに関連付けます。

私の例では、空のアキュムレータを作成する方法と、このアキュムレータを使用して要素を配列自体の要素から連結する方法の情報を抽出できるため、単一の引数としてreduceのみを期待する関数を有効にしますArray

Map弱い参照は、ガベージコレクションされない組み込みのプロトタイプを表すだけの場合は意味がないため、通常のタイプを使用できたかもしれないことに注意してください。ただし、a WeakMapは反復可能ではなく、適切なキーがないと検査できません。型のリフレクションは避けたいので、これは望ましい機能です。


これはWeakMapのクールな使い方です。xs[0]空の配列については注意してください。type(undefined).monoid ...
ありがとう

@naomikわかっています-私の最新の質問の2番目のコードスニペットを参照してください。

この@ftorのサポートはどの程度標準ですか?
onassar 2017年

5
-1; これの何のポイントは何ですか?メソッドにタイプをマッピングする個別のグローバルディクショナリを作成し、タイプによってそのディクショナリ内のメソッドを明示的に検索するだけです。その結果、継承のサポートが失われ(Object.prototypeこのようにして「追加」されたメソッドをで呼び出すことはできませんArray)、構文はプロトタイプを本当に拡張した場合よりも大幅に長く/醜くなります。ほとんどの場合、静的メソッドでユーティリティクラスを作成する方が簡単です。このアプローチの唯一の利点は、ポリモーフィズムのサポートが制限されていることです。
Mark Amery 2017

4
@MarkAmeryこんにちは、あなたはそれを取得できません。私は継承を失うことはありませんが、それを取り除きます。継承は80年代なので、忘れてください。このハックの要点は、単純にオーバーロードされた関数である型クラスを模倣することです。ただし、正当な批判があります。JavaScriptで型クラスを模倣することは有用ですか?いいえ、そうではありません。むしろ、それらをネイティブにサポートする型付き言語を使用します。これがあなたの意図したことだと確信しています。

7

ネイティブオブジェクトを拡張してはならないもう1つの理由:

私たちはprototype.jsを使用するMagentoを使用しています、ネイティブオブジェクトの多くの機能を拡張。これは、新機能の導入を決定するまでは問題なく機能し、大きな問題が発生します。

ページの1つにWebコンポーネントを導入したので、webcomponents-lite.jsはIEの(ネイティブ)イベントオブジェクト全体を置き換えることにしました(なぜですか?)。もちろんこれはprototype.jsを壊しますをし、それがMagentoを破壊します。(問題が見つかるまで、トレースに何時間も費やす可能性があります)

トラブルが気になったら、続けてください!


4

これを行わない理由は3つあります(少なくともアプリケーション内から)。そのうちの2つだけが、既存の回答でここに取り上げられています。

  1. 間違えると、拡張型のすべてのオブジェクトに列挙可能なプロパティを誤って追加してしまいます。を使用して簡単に回避Object.definePropertyデフォルトで列挙不可能なプロパティを作成するます。
  2. 使用しているライブラリと競合する可能性があります。勤勉で回避することができます。プロトタイプに何かを追加する前に、使用するライブラリが定義するメソッドを確認し、アップグレード時にリリースノートを確認して、アプリケーションをテストしてください。
  3. ネイティブJavaScript環境の将来のバージョンと競合する可能性があります。

ポイント3は間違いなく最も重要なものです。使用するライブラリ決定するので、プロトタイプ拡張が使用するライブラリと競合しないことをテストを通じて確認できます。コードがブラウザーで実行されると仮定すると、ネイティブオブジェクトには同じことが当てはまりません。Array.prototype.swizzle(foo, bar)今日を定義し、明日GoogleがArray.prototype.swizzle(bar, foo)Chromeに追加する場合、何故か疑問に思う混乱した同僚になってしまうでしょう。.swizzle MDNで文書化されている動作と動作が一致しないように見えるの。

彼らが所有していないプロトタイプをmootoolsがいじっているという話も参照して、Webの破壊を避けるためにES6メソッドの名前を強制的に変更しました。)

これは、ネイティブオブジェクトに追加されたメソッドにアプリケーション固有のプレフィックスを使用することで回避できます(たとえば、のArray.prototype.myappSwizzle代わりに定義するArray.prototype.swizzle)が、これは一種の醜いものです。これは、プロトタイプを拡張する代わりにスタンドアロンユーティリティ関数を使用することでも解決できます。


2

パフォーマンスも理由です。キーをループする必要がある場合があります。これを行うにはいくつかの方法があります

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

私は通常for of Object.keys()、それが正しいことを行い、比較的簡潔で、チェックを追加する必要がないので使用します。

しかし、それははるかに遅いです。

for-ofとfor-inのパフォーマンス結果

理由Object.keysが遅いと推測するのは明らかですObject.keys()が、割り当てを行う必要があります。実際、私の知る限り、それ以降、すべてのキーのコピーを割り当てる必要があります。

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

JSエンジンがなんらかの不変キー構造を使用して、不変キーObject.keys(object)を反復処理しobject.newProp、まったく新しい不変キーオブジェクトを作成するような参照を返す可能性がありますが、明らかに最大で15倍遅くなります

チェックすらhasOwnProperty最大2倍遅くなります。

これらすべての要点は、perfセンシティブなコードがあり、キーをループする必要がある場合for in、を呼び出さなくても使用できるようにしたいということですhasOwnProperty。変更していない場合にのみ、これを行うことができますObject.prototype

を使用Object.definePropertyしてプロトタイプを変更した場合、追加したものが列挙できない場合は、上記の場合のJavaScriptの動作には影響しません。残念ながら、少なくともChrome 83では、パフォーマンスに影響を与えます。

ここに画像の説明を入力してください

パフォーマンスの問題が発生するように強制するために、3000の列挙できないプロパティを追加しました。プロパティが30個しかない場合、テストはパフォーマンスに影響があるかどうかを判断するには近すぎます。

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77とSafari 13.1では、拡張されたクラスと拡張されていないクラスのパフォーマンスに違いはありませんでした。v8はこの領域で修正され、パフォーマンスの問題は無視できます。

しかし、の話もありArray.prototype.smooshます。ショートバージョンは人気のライブラリであるMootoolsで、独自に作成されましたArray.prototype.flatten。標準化委員会がネイティブを追加しようとしたとき、Array.prototype.flatten彼らは多くのサイトを壊すことなしには不可能を見つけました。ブレークについて知った開発者は、es5メソッドsmooshをジョークと名付けることを提案しましたが、人々はそれがジョークであると理解せずにびっくりしました。彼らはのflat代わりに解決しましたflatten

この話の教訓は、ネイティブオブジェクトを拡張すべきではないということです。そうした場合、同じ問題が発生し、特定のライブラリがMooToolsと同じくらい人気がなければ、ブラウザベンダーが原因で問題を回避することはできません。ライブラリがその人気を博した場合、あなたが引き起こした問題を他のすべての人に回避させることは一種の意味になるでしょう。したがって、ネイティブオブジェクトを拡張しないでください。


待ちは、hasOwnPropertyプロパティはオブジェクトやプロトタイプであるかどうかを示します。しかし、for inループは列挙可能なプロパティのみを取得します。私はこれを試してみました:列挙できないプロパティをArrayプロトタイプに追加すると、ループに表示されません。最も単純なループが機能するようにプロトタイプを変更する必要ありません。
ygoe

しかし、それは他の理由で依然として悪い習慣です;)
gman
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.