Yerel nesneleri genişletmek neden kötü bir uygulamadır?


136

Her JS fikir lideri, yerel nesneleri genişletmenin kötü bir uygulama olduğunu söylüyor. Ama neden? Bir performans yakalayacak mıyız? Birinin bunu "yanlış şekilde" yapmasından Objectve herhangi bir nesnedeki tüm döngüleri pratik olarak yok ederek sayısız türler eklemesinden mi korkuyorlar ?

Al TJ Holowaychuk bireyin should.js örneğin. O basit bir getter ekler için Objectve her şey para cezası (çalışır kaynak ).

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

Bu gerçekten mantıklı. Örneğin biri genişletilebilir Array.

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

Yerel türleri genişletmeye karşı herhangi bir argüman var mı?


9
Daha sonra, yerel bir nesne sizinkinden farklı anlamlara sahip bir "kaldır" işlevi içerecek şekilde değiştirildiğinde ne olmasını bekliyorsunuz? Standardı kontrol etmiyorsun.
ta.speot .is

1
Senin yerli tipin değil. Herkesin yerli tipi.

6
"Birinin bunu" yanlış şekilde "yapmasından ve Object'e sayısız türler ekleyerek herhangi bir nesnedeki tüm döngüleri pratik olarak yok edeceğinden mi korkuyorlar?" : Evet. Bu fikrin oluştuğu günlerde, sayılamayan mülkler yaratmak imkansızdı. Şimdi bu konuda işler farklı olabilir, ancak her kütüphanenin yerel nesneleri istedikleri gibi genişlettiğini hayal edin. Ad alanlarını kullanmaya başlamamızın bir nedeni var.
Felix Kling

5
Bazı "kanaat önderleri" ne değerse, örneğin Brendan Eich, yerel prototipleri genişletmenin mükemmel olduğunu düşünüyor.
Benjamin Gruenbaum

2
Foo bu günlerde küresel olmamalı, dahil ettik, requirejs, commonjs vb.
Jamie Pate

Yanıtlar:


128

Bir nesneyi genişlettiğinizde, davranışını değiştirirsiniz.

Yalnızca kendi kodunuz tarafından kullanılacak bir nesnenin davranışını değiştirmek sorun değildir. Ancak başka kodlar tarafından da kullanılan bir şeyin davranışını değiştirdiğinizde, diğer kodu kırma riski vardır.

JavaScript'te nesne ve dizi sınıflarına yöntemler eklemek söz konusu olduğunda, javascript'in çalışma biçimi nedeniyle bir şeyi kırma riski çok yüksektir. Uzun yıllara dayanan deneyimlerim bana bu tür şeylerin javascript'te her türlü korkunç hataya neden olduğunu öğretti.

Özel davranışa ihtiyacınız varsa, yerel olanı değiştirmek yerine kendi sınıfınızı (belki bir alt sınıf) tanımlamak çok daha iyidir. Bu şekilde hiçbir şeyi kırmayacaksınız.

Bir sınıfın alt sınıflandırma yapmadan nasıl çalıştığını değiştirme yeteneği, herhangi bir iyi programlama dilinin önemli bir özelliğidir, ancak nadiren ve dikkatli kullanılması gereken bir özelliktir.


2
Yani böyle bir şey eklemek .stgringify()güvenli kabul edilir mi?
buschtoens

7
Pek çok sorun var, örneğin başka bir kod da stringify()farklı davranışlarla kendi yöntemini eklemeye çalışırsa ne olur ? Bu gerçekten günlük programlamada yapman gereken bir şey değil ... tek yapmak istediğin, arada bir birkaç karakter kodu kaydetmek değilse. Herhangi bir girdiyi kabul eden ve dizgeleyen kendi sınıfınızı veya işlevinizi tanımlamak daha iyidir. Çoğu tarayıcı tanımlıyor JSON.stringify(), en iyisi bunun var olup olmadığını kontrol etmek ve eğer bunu kendiniz tanımlamamaktır.
Abhi Beckert

5
Ben tercih someError.stringify()üzerinde errors.stringify(someError). Basittir ve js konseptine mükemmel bir şekilde uyar. Özellikle belirli bir ErrorObject'e bağlı bir şey yapıyorum.
buschtoens

1
Geriye kalan tek (ama iyi) argüman, bazı devasa makro-liblerin türlerinizi ele geçirmeye başlayabileceği ve birbirlerini etkileyebileceğidir. Yoksa başka bir şey var mı?
buschtoens

4
Sorun bir çarpışmanız olabileceğiyse, bunu her yaptığınızda, tanımlamadan önce her zaman işlevin tanımsız olduğunu doğruladığınız bir model kullanabilir misiniz? Ve her zaman üçüncü taraf kitaplıklarını kendi kodunuzun önüne koyarsanız, bu tür çakışmaları her zaman algılayabilmelisiniz.
Dave Cousineau

32

Performans vuruşu gibi ölçülebilir bir dezavantaj yok. En azından kimse bundan bahsetmedi. Yani bu kişisel tercih ve deneyimlerle ilgili bir sorudur.

Temel profesyonel argüman: Daha iyi görünüyor ve daha sezgisel: sözdizimi şekeri. Tipe / örneğe özgü bir işlevdir, bu nedenle özellikle bu türe / örneğe bağlı olmalıdır.

Ana karşı argüman: Kod müdahale edebilir. Lib A bir işlev eklerse, lib B'nin işlevinin üzerine yazabilir. Bu, kodu çok kolay bir şekilde bozabilir.

Her ikisinin de bir anlamı var. Türlerinizi doğrudan değiştiren iki kitaplığa güvendiğinizde, beklenen işlev muhtemelen aynı olmadığından büyük olasılıkla bozuk kodla karşılaşacaksınız. Buna tamamen katılıyorum. Makro kitaplıkları yerel türleri değiştirmemelidir. Aksi takdirde, bir geliştirici olarak perde arkasında gerçekte neler olup bittiğini asla bilemezsiniz.

Ve jQuery, alt çizgi gibi kitaplardan hoşlanmamamın nedeni budur. Beni yanlış anlamayın; kesinlikle iyi programlanmışlar ve bir cazibe gibi çalışıyorlar, ama büyükler . Bunların yalnızca% 10'unu kullanıyorsunuz ve yaklaşık% 1'ini anlıyorsunuz.

Bu yüzden , sadece gerçekten ihtiyacınız olana ihtiyaç duyduğunuz atomistik bir yaklaşımı tercih ediyorum . Bu şekilde ne olacağını her zaman bilirsiniz. Mikro kütüphaneler sadece sizin yapmalarını istediğinizi yaparlar, böylece müdahale etmezler. Son kullanıcının hangi özelliklerin eklendiğini bilmesi bağlamında, yerel türlerin genişletilmesi güvenli kabul edilebilir.

TL; DR Şüpheye düştüğünüzde yerel türleri genişletmeyin. Yerel bir türü yalnızca son kullanıcının bu davranışı bilip isteyeceğinden% 100 eminseniz genişletin. Gelen hiçbir mevcut arayüzünü kırmak gibi bir durumda, bir yerli türünün mevcut fonksiyonlarını manipüle.

Yazıyı genişletmeye karar verirseniz, kullanın Object.defineProperty(obj, prop, desc); yapamıyorsanız , türlerini kullanın prototype.


Başlangıçta bu soruyla geldim çünkü Errore-postaların JSON aracılığıyla gönderilebilir olmasını istedim . Bu yüzden onları sertleştirmenin bir yolunu bulmalıydım. error.stringify()daha iyi hissetti errorlib.stringify(error); İkinci yapının önerdiği gibi, kendi üzerinde errorlibdeğil, üzerinde çalışıyorum error.


Bu konuda başka görüşlere açığım.
buschtoens

4
JQuery ve alt çizginin yerel nesneleri genişlettiğini mi ima ediyorsunuz? Yapmazlar. Yani bu nedenle onlardan kaçıyorsanız, yanılıyorsunuz.
JLRishe

1
Burada, yerel nesneleri lib A ile genişletmenin lib B ile çelişebileceğine dair argümanda gözden kaçtığına inandığım bir soru var: Neden doğası gereği çok benzer veya doğası gereği o kadar geniş olan iki kitaplığınız olsun ki bu çelişki mümkün olabilir? Yani ya lodash'ı seçiyorum ya da altını çiziyorum. Javascript'teki mevcut
libraray ortamımız

2
@micahblu - bir kütüphane, sadece kendi dahili programlamasının rahatlığı için standart bir nesneyi değiştirmeye karar verebilir (bu yüzden insanlar genellikle bunu yapmak ister). Yani, aynı işleve sahip iki kitaplık kullanmayacağınız için bu çatışmadan kaçınılmaz.
jfriend00

1
Ayrıca (es2015 itibariyle) mevcut bir sınıfı genişleten yeni bir sınıf oluşturabilir ve bunu kendi kodunuzda kullanabilirsiniz. yani diğer alt sınıflarla çarpışmadan MyError extends Errorbir stringifyyöntemi olabilir . Yine de kendi kodunuz tarafından oluşturulmayan hataları işlemeniz gerekir, bu nedenle Errordiğerlerine göre daha az yararlı olabilir .
ShadSterling

16

Bence bu kötü bir uygulama. Bunun başlıca nedeni entegrasyondur. Should.js dokümanlarından alıntı yapma:

OMG IT NESNEYİ GENİŞLETİYOR ???!?! @ Evet, evet öyle, tek bir alıcı ile gerekir ve hayır kodunuzu kırmaz

Peki yazar nasıl bilebilir? Ya alay çerçevem ​​de aynısını yaparsa? Ya sözlerim lib de aynısını yaparsa?

Bunu kendi projenizde yapıyorsanız sorun değil. Ama bir kütüphane için kötü bir tasarım. Underscore.js, doğru şekilde yapılan şeyin bir örneğidir:

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

2
Eminim TJ'nin cevabı bu vaatleri veya alaycı çerçeveleri kullanmamak olacaktır: x
Jim Schubert

_(arr).flatten()Örnek aslında yerli nesneleri genişletmek için değil beni ikna etti. Bunu yapmak için kişisel nedenim tamamen sözdizimseldi. Ama bu benim estetik hislerimi tatmin ediyor :) foo(native).coolStuff()Onu "genişletilmiş" bir nesneye dönüştürmek gibi daha normal bir işlev adı kullanmak bile sözdizimsel olarak harika görünüyor. Bunun için teşekkürler!
egst

16

Buna duruma göre bakarsanız, belki bazı uygulamalar kabul edilebilir.

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

Önceden oluşturulmuş yöntemlerin üzerine yazmak, çözdüğünden daha fazla sorun yaratır, bu nedenle, bu uygulamadan kaçınmak için birçok programlama dilinde yaygın olarak belirtilmesinin nedeni budur. Devs, işlevin değiştirildiğini nasıl anlayabilir?

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

Bu durumda, bilinen herhangi bir çekirdek JS yönteminin üzerine yazmıyoruz, ancak String'i genişletiyoruz. Bu gönderideki bir argüman, yeni geliştiricinin bu yöntemin çekirdek JS'nin bir parçası olup olmadığını veya belgeleri nerede bulacağını nasıl bildiğinden bahsetti. Çekirdek JS String nesnesi denen bir yöntem olsun olsaydı ne olurdu yararlanmak ?

Ya diğer kitaplıklarla çakışabilecek isimler eklemek yerine, tüm geliştiricilerin anlayabileceği şirkete / uygulamaya özel bir değiştirici kullandıysanız?

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.

Diğer nesneleri genişletmeye devam ederseniz, tüm geliştiriciler bunlarla vahşi ortamda karşılaşırdı (bu durumda), biz de onlara bunun şirkete / uygulamaya özel bir uzantı olduğunu bildiririz.

Bu, isim çakışmalarını ortadan kaldırmaz, ancak olasılığı azaltır. Çekirdek JS nesnelerini genişletmenin sizin ve / veya ekibiniz için olduğunu belirlerseniz, belki bu sizin içindir.


7

Yerleşik prototiplerin genişletilmesi gerçekten kötü bir fikirdir. Bununla birlikte, ES2015, istenen davranışı elde etmek için kullanılabilecek yeni bir teknik tanıttı:

WeakMapTürleri yerleşik prototiplerle ilişkilendirmek için s'yi kullanma

Aşağıdaki uygulama, Numberve Arrayprototiplerini onlara hiç dokunmadan genişletir :

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

Gördüğünüz gibi ilkel prototipler bile bu teknikle genişletilebilir. ObjectTürleri yerleşik prototiplerle ilişkilendirmek için bir harita yapısı ve kimliği kullanır .

Örneğim, bir reduceişlevi yalnızca Arraytek bağımsız değişkeni olarak bekleyen bir işlevi etkinleştirir , çünkü boş bir toplayıcının nasıl oluşturulacağı ve bu toplayıcıyla öğelerin Dizinin öğelerinden nasıl birleştirileceği bilgisini çıkarabilir.

Lütfen normal Maptürü kullanmış olabileceğime dikkat edin , çünkü zayıf referanslar yalnızca yerleşik prototipleri temsil ettiklerinde ve asla çöp toplanmayan yerleşik prototipleri temsil ettiklerinde anlamsızdır. Ancak, a WeakMapyinelenemez ve doğru anahtara sahip değilseniz incelenemez. Herhangi bir tür yansımasından kaçınmak istediğim için bu istenen bir özelliktir.


Bu WeakMap'in harika bir kullanımıdır. xs[0]Boş dizilerde dikkatli olun tho. type(undefined).monoid ...
teşekkür ederim

@naomik biliyorum - en son soruma bakın 2. kod pasajı.

Bu @ftor için destek ne kadar standart?
onassar

5
1; bunların herhangi birinin anlamı nedir? Yöntemlerle yalnızca ayrı bir genel sözlük eşleme türleri oluşturuyorsunuz ve ardından bu sözlükte türe göre yöntemleri açıkça arıyorsunuz. Sonuç olarak, kalıtım için desteğinizi kaybedersiniz (bir yöntemObject.prototype bu şekilde bir üzerinde çağrılamaz Array) ve sözdizimi, bir prototipi gerçekten genişlettiğinizden önemli ölçüde daha uzun / çirkin olur. Statik yöntemlerle yardımcı sınıflar oluşturmak neredeyse her zaman daha kolay olurdu; Bu yaklaşımın sahip olduğu tek avantaj, polimorfizm için bazı sınırlı destektir.
Mark Amery

4
@MarkAmery Hey dostum, anlamıyorsun. Mirası kaybetmiyorum ama ondan kurtuluyorum. Miras çok 80'ler, unutmalısın. Bu hacklemenin tüm amacı, basitçe ifade etmek gerekirse, aşırı yüklenmiş fonksiyonlar olan tip sınıflarını taklit etmektir. Yine de meşru bir eleştiri var: Javascript'teki tip sınıflarını taklit etmek faydalı mı? Hayır, değil. Bunun yerine, onları yerel olarak destekleyen yazılı bir dil kullanın. Kastettiğin bu olduğundan oldukça eminim.

7

Eğer neden bir sebep daha değil yerli Nesneleri uzatmak:

Prototype.js kullanan ve yerel Nesneler üzerinde birçok şeyi genişleten Magento kullanıyoruz . Bu, yeni özellikler almaya karar verene kadar iyi çalışır ve büyük sorunların başladığı yer burasıdır.

Web bileşenlerini sayfalarımızdan birinde tanıttık, bu nedenle webcomponents-lite.js IE'deki tüm (yerel) Etkinlik Nesnesini değiştirmeye karar verir (neden?). Bu elbette prototype.js'yi bozar ve bu da Magento'yu bozar. (sorunu bulana kadar, onu geriye doğru izlemek için çok fazla saat harcayabilirsiniz)

Eğer beladan hoşlanıyorsanız, yapmaya devam edin!


4

Bunu yapmamak için üç neden görebiliyorum ( en azından bir uygulama içinden ), bunlardan sadece ikisi burada mevcut cevaplarda ele alınmıştır:

  1. Yanlış yaparsanız, yanlışlıkla genişletilmiş türdeki tüm nesnelere numaralandırılabilir bir özellik eklersiniz. Object.definePropertyVarsayılan olarak numaralandırılamayan özellikler oluşturan, kullanarak kolayca çalışıldı .
  2. Kullandığınız bir kitaplıkla çatışmaya neden olabilirsiniz. Titizlikle önlenebilir; bir prototipe bir şey eklemeden önce kullandığınız kitaplıkların hangi yöntemleri tanımladığını kontrol edin, yükseltme sırasında sürüm notlarını kontrol edin ve uygulamanızı test edin.
  3. Yerel JavaScript ortamının gelecekteki bir sürümüyle çakışmaya neden olabilirsiniz.

3. nokta tartışmasız en önemlisidir. Test ederek, prototip uzantılarınızın kullandığınız kitaplıklarla herhangi bir çakışmaya neden olmadığından emin olabilirsiniz, çünkü hangi kitaplıkları kullanacağınıza siz karar verirsiniz. Aynı durum, kodunuzun bir tarayıcıda çalıştığını varsayarak yerel nesneler için geçerli değildir. Array.prototype.swizzle(foo, bar)Bugün tanımlarsanız ve yarın Google Array.prototype.swizzle(bar, foo)Chrome'a eklerse , nedenini merak eden bazı kafası karışmış meslektaşlarınızla karşılaşabilirsiniz..swizzle , MDN'de belgelenen davranışların uyuşmadığını karşılaşabilirsiniz.

( Mootoollerin sahip olmadıkları prototiplerle uğraşmasının, ağı bozmamak için bir ES6 yöntemini yeniden adlandırmaya nasıl zorladığının hikayesine de bakın. .)

Bu, yerel nesnelere eklenen yöntemler için uygulamaya özel bir önek kullanılarak önlenebilir (örneğin, Array.prototype.myappSwizzleyerine tanımla Array.prototype.swizzle), ancak bu biraz çirkin; prototipleri çoğaltmak yerine bağımsız yardımcı program işlevlerini kullanarak çözülebilir.


2

Perf aynı zamanda bir nedendir. Bazen anahtarların üzerinden geçmeniz gerekebilir. Bunu yapmanın birkaç yolu var

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

Genelde for of Object.keys()doğru şeyi yaptığı için kullanıyorum ve nispeten kısa, çeki eklemeye gerek yok.

Ama çok daha yavaştır .

for-of vs for-in perf sonuçları

Sebebinin Object.keysyavaş olduğunu tahmin etmek bile belli, Object.keys()bir tahsis yapmak zorunda. Aslında AFAIK, o zamandan beri tüm anahtarların bir kopyasını tahsis etmek zorundadır.

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

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

JS motorunun bir çeşit değişmez anahtar yapısı kullanması mümkündür, böylece Object.keys(object)değişmez anahtarlar üzerinde yinelenen ve object.newProptamamen yeni bir değişmez anahtar nesnesi oluşturan bir referans şey döndürür , ancak her neyse, açıkça 15 kata kadar daha yavaştır.

Kontrol bile hasOwnProperty2 kata kadar daha yavaştır.

Bütün bunların amacı, eğer mükemmel derecede hassas bir kodunuz varsa ve anahtarlar üzerinde döngü yapmanız gerekiyorsa, o zaman for inaramak zorunda kalmadan kullanmak isteyebilirsiniz hasOwnProperty. Bunu yalnızca değiştirmediyseniz yapabilirsinizObject.prototype

Object.definePropertyPrototipi değiştirmek için kullanırsanız , eklediğiniz şeyler numaralandırılamazsa, yukarıdaki durumlarda JavaScript'in davranışını etkilemeyeceğini unutmayın. Ne yazık ki, en azından Chrome 83'te performansı etkiliyorlar.

görüntü açıklamasını buraya girin

Herhangi bir performans sorununu ortaya çıkmaya zorlamak için 3000 numaralandırılamayan özellik ekledim. Yalnızca 30 özellik ile testler, herhangi bir mükemmel etki olup olmadığını söyleyemeyecek kadar yakındı.

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

Firefox 77 ve Safari 13.1, Augmented ve Unaugmented sınıfları arasında performans açısından hiçbir fark göstermedi, belki v8 bu alanda düzeltilecek ve performans sorunlarını görmezden gelebilirsiniz.

Ama şunu da ekleyeyim hikayesi varArray.prototype.smoosh . Kısa versiyon, popüler bir kütüphane olan Mootools'dur Array.prototype.flatten. Standartlar komitesi bir yerli eklemeye çalıştığında, pek Array.prototype.flattençok siteyi kırmadan yapamayacağını buldular. Arayı öğrenen geliştiriciler, es5 yöntemini smooshbir şaka olarak adlandırmayı önerdiler, ancak insanlar bunun bir şaka olduğunu anlamamasından korktular . Onlar yerleşmiş flatyerineflatten

Hikayenin ahlaki, yerel nesneleri genişletmemeniz gerektiğidir. Bunu yaparsanız, eşyalarınızın kırılmasıyla ilgili aynı sorunla karşılaşabilirsiniz ve belirli kitaplığınız MooTools kadar popüler olmadıkça, tarayıcı satıcılarının neden olduğunuz sorunu çözme olasılığı düşüktür. Kitaplığınız bu kadar popüler hale gelirse, neden olduğunuz sorunla ilgili olarak herkesi çalışmaya zorlamak bir tür anlamsızlık olur. Bu nedenle, lütfen Yerel Nesneleri Genişletmeyin


Bekle, hasOwnPropertyözelliğin nesnede mi yoksa prototipte mi olduğunu söyler. Ancak for indöngü size yalnızca numaralandırılabilir özellikler kazandırır. Bunu biraz önce denedim: Array prototipine numaralandırılamayan bir özellik ekleyin ve döngüde görünmeyecek. En basit döngünün çalışması için prototipi değiştirmeye gerek yoktur .
ygoe

Ama yine de başka nedenlerden dolayı kötü bir uygulama;)
gman
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.