web-dev-qa-db-ja.com

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

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

TJ Holowaychukshould.js を例にとります。彼は 単純なゲッターを追加Objectを追加し、すべて正常に動作します( source )。

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

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

115
buschtoens

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

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

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

カスタム動作が必要な場合は、ネイティブクラスを変更するのではなく、独自のクラス(おそらくサブクラス)を定義することをお勧めします。そうすれば、何も壊しません。

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

108
Abhi Beckert

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

主なproの引数:見栄えが良く、より直感的です:構文シュガー。これはタイプ/インスタンス固有の関数であるため、そのタイプ/インスタンスに特にバインドする必要があります。

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

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

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

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

TL; DR疑わしい場合は、ネイティブ型を拡張しないでください。ネイティブユーザーが100%確信している場合にのみ、ネイティブユーザーを拡張して、エンドユーザーがその動作を認識して望んでいることを確認してください。 noの場合、ネイティブ型の既存の関数を操作します。既存のインターフェイスが破損するためです。

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


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

27
buschtoens

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

OMG ITがオブジェクトを拡張する???????!@はい、はい

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

あなた自身のプロジェクトでそれをしているなら、それは大丈夫です。しかし、図書館にとっては、それは悪い設計です。 Underscore.jsは、正しい方法で行われたものの例です。

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

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

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オブジェクトを拡張するのが自分やチームのためであると判断した場合、おそらくこれがあなたのためです。

10
SoEzPz

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

WeakMapsを使用して、型を組み込みプロトタイプに関連付けます

次の実装は、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型を使用できたことに注意してください。これは、ガベージコレクションされない組み込みプロトタイプを表すだけでは弱い参照は意味をなさないためです。ただし、WeakMapは反復可能ではなく、正しいキーがない限り検査できません。これは、任意の形式のタイプリフレクションを避けたいため、望ましい機能です。

7
user6445533

notネイティブオブジェクトを拡張する必要があるもう1つの理由:

prototype.jsを使用し、ネイティブオブジェクトの多くのものを拡張するMagentoを使用します。これは、新しい機能を導入することを決定するまで問題なく機能し、そこから大きなトラブルが始まります。

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

あなたがトラブルが好きなら、それをやり続けてください!

3
Eugen Wesseloh

私はこれをしない3つの理由を見ることができます(少なくともアプリケーション内から)、ここでの既存の回答ではそのうちの2つだけが対処されています:

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

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

所有していないプロトタイプをmootoolsがいじる方法のストーリーも参照してください。Webの破損を避けるためにES6メソッドの名前を変更する必要がありました 。)

これは、ネイティブオブジェクトに追加されたメソッドにアプリケーション固有のプレフィックスを使用することで回避できます(たとえば、_Array.prototype.myappSwizzle_の代わりに_Array.prototype.swizzle_を定義します)が、これはちょっといです。プロトタイプを増補する代わりに、スタンドアロンのユーティリティ関数を使用することで同様に解決できます。

1
Mark Amery