Mengapa memperluas objek asli adalah praktik yang buruk?

136

Setiap pemimpin opini JS mengatakan bahwa memperluas objek asli adalah praktik yang buruk. Tapi kenapa? Apakah kita mendapatkan kinerja yang baik? Apakah mereka takut seseorang melakukan itu dengan "cara yang salah", dan menambahkan tipe yang dapat dihitung Object, secara praktis menghancurkan semua loop pada objek apa pun?

Ambil TJ Holowaychuk 's should.js misalnya. Dia menambahkan getter sederhana untuk Objectdan semuanya bekerja dengan baik ( sumber ).

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

Ini sangat masuk akal. Misalnya seseorang dapat memperpanjang Array.

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

Apakah ada argumen yang menentang perluasan tipe asli?

buschtoens
sumber
9
Apa yang Anda harapkan terjadi ketika, kemudian, objek asli diubah untuk menyertakan fungsi "hapus" dengan semantik yang berbeda dengan Anda sendiri? Anda tidak mengontrol standar.
ta.speot.is
1
Itu bukan tipe asli Anda. Ini tipe asli setiap orang.
6
"Apakah mereka takut seseorang melakukan itu" dengan cara yang salah ", dan menambahkan tipe enumerable ke Object, praktis menghancurkan semua loop pada objek apa pun?" : Yap. Kembali pada hari-hari ketika opini ini dibentuk, adalah mustahil untuk membuat properti yang tidak dapat dihitung. Sekarang hal mungkin berbeda dalam hal ini, tetapi bayangkan setiap perpustakaan hanya memperluas objek asli seperti yang mereka inginkan. Ada alasan mengapa kami mulai menggunakan ruang nama.
Felix Kling
5
Untuk apa itu layak beberapa "pemimpin opini" misalnya Brendan Eich berpikir itu baik-baik saja untuk memperpanjang prototipe asli.
Benjamin Gruenbaum
2
Foo seharusnya tidak menjadi global hari ini, kami telah menyertakan, requireejs, commonjs dll.
Jamie Pate

Jawaban:

127

Saat Anda memperluas objek, Anda mengubah perilakunya.

Mengubah perilaku objek yang hanya akan digunakan oleh kode Anda sendiri tidak masalah. Tetapi ketika Anda mengubah perilaku sesuatu yang juga digunakan oleh kode lain ada risiko Anda akan merusak kode lain itu.

Ketika datang menambahkan metode ke objek dan kelas array di javascript, risiko melanggar sesuatu sangat tinggi, karena cara kerja javascript. Pengalaman bertahun-tahun telah mengajarkan saya bahwa hal-hal semacam ini menyebabkan semua jenis bug yang mengerikan di javascript.

Jika Anda memerlukan perilaku khusus, jauh lebih baik untuk mendefinisikan kelas Anda sendiri (mungkin subkelas) daripada mengubah yang asli. Dengan begitu Anda tidak akan merusak apa pun.

Kemampuan untuk mengubah cara kerja kelas tanpa mensubclassing itu adalah fitur penting dari setiap bahasa pemrograman yang baik, tetapi itu adalah salah satu yang harus jarang digunakan dan dengan hati-hati.

Abhi Beckert
sumber
2
Jadi menambahkan sesuatu seperti .stgringify()akan dianggap aman?
buschtoens
7
Ada banyak masalah, misalnya apa yang terjadi jika beberapa kode lain juga mencoba menambahkan stringify()metode sendiri dengan perilaku yang berbeda? Ini benar-benar bukan sesuatu yang harus Anda lakukan dalam pemrograman sehari-hari ... tidak jika semua yang ingin Anda lakukan adalah menyimpan beberapa karakter kode di sana-sini. Lebih baik untuk mendefinisikan kelas atau fungsi Anda sendiri yang menerima input apa pun dan mengencangkannya. Sebagian besar browser menentukan JSON.stringify(), yang terbaik adalah memeriksa apakah itu ada, dan jika tidak menentukannya sendiri.
Abhi Beckert
5
Saya lebih suka someError.stringify()lebih errors.stringify(someError). Sangat mudah dan sangat sesuai dengan konsep js. Saya melakukan sesuatu yang secara spesifik terikat pada ErrorObject tertentu.
buschtoens
1
Satu-satunya argumen (tapi bagus) yang tersisa adalah, bahwa beberapa lib makro-besar dapat mulai mengambil alih tipe Anda dan dapat saling mengganggu. Atau ada yang lain?
buschtoens
4
Jika masalahnya adalah Anda mungkin memiliki tabrakan, bisakah Anda menggunakan pola di mana setiap kali Anda melakukan ini, Anda selalu mengonfirmasi bahwa fungsi tidak terdefinisi sebelum Anda mendefinisikannya? Dan jika Anda selalu menempatkan pustaka pihak ketiga di depan kode Anda sendiri, Anda harus selalu dapat mendeteksi tabrakan tersebut.
Dave Cousineau
32

Tidak ada kelemahan yang terukur, seperti hit kinerja. Setidaknya tidak ada yang menyebutkan. Jadi ini adalah pertanyaan tentang preferensi dan pengalaman pribadi.

Argumen pro utama: Terlihat lebih baik dan lebih intuitif: sintaksis gula. Ini adalah fungsi spesifik tipe / instance, jadi harus secara spesifik terikat pada tipe / instance tersebut.

Argumen kontra utama: Kode dapat mengganggu. Jika lib A menambahkan fungsi, itu bisa menimpa fungsi lib B. Ini dapat memecahkan kode dengan sangat mudah.

Keduanya ada benarnya. Ketika Anda mengandalkan dua pustaka yang secara langsung mengubah jenis Anda, kemungkinan besar Anda akan berakhir dengan kode yang rusak karena fungsi yang diharapkan mungkin tidak sama. Saya setuju sepenuhnya tentang itu. Perpustakaan makro tidak boleh memanipulasi tipe asli. Kalau tidak, Anda sebagai pengembang tidak akan pernah tahu apa yang sebenarnya terjadi di balik layar.

Dan itulah alasan saya tidak suka lib seperti jQuery, garis bawah, dll. Jangan salah paham; mereka benar-benar diprogram dengan baik dan mereka bekerja seperti pesona, tetapi mereka besar . Anda hanya menggunakan 10% dari mereka, dan mengerti tentang 1%.

Itu sebabnya saya lebih suka pendekatan atomistik , di mana Anda hanya membutuhkan apa yang benar-benar Anda butuhkan. Dengan cara ini, Anda selalu tahu apa yang terjadi. Perpustakaan mikro hanya melakukan apa yang Anda inginkan, sehingga tidak akan mengganggu. Dalam konteks memiliki pengguna akhir yang mengetahui fitur mana yang ditambahkan, memperluas tipe asli dapat dianggap aman.

TL; DR Jika ragu, jangan berikan jenis asli. Hanya perluas jenis asli jika Anda 100% yakin, bahwa pengguna akhir akan mengetahui dan menginginkan perilaku itu. Dalam tidak ada kasus memanipulasi fungsi yang ada jenis pribumi, karena akan mematahkan antarmuka yang ada.

Jika Anda memutuskan untuk memperluas jenisnya, gunakan Object.defineProperty(obj, prop, desc); jika tidak bisa , gunakan jenisnya prototype.


Saya awalnya datang dengan pertanyaan ini karena saya ingin Errordapat dikirim melalui JSON. Jadi, saya perlu cara untuk mengikat mereka. error.stringify()merasa jauh lebih baik daripadaerrorlib.stringify(error) ; seperti saran kedua, saya beroperasi errorlibdan tidak dengan errorsendirinya.

buschtoens
sumber
Saya terbuka pada pendapat lebih lanjut tentang ini.
buschtoens
4
Apakah Anda menyiratkan bahwa jQuery dan garis bawah memperpanjang objek asli? Mereka tidak melakukannya. Jadi jika Anda menghindari mereka karena alasan itu, Anda salah.
JLRishe
1
Berikut adalah pertanyaan yang saya yakin tidak terjawab dalam argumen bahwa memperluas objek asli dengan lib A dapat bertentangan dengan lib B: Mengapa Anda memiliki dua perpustakaan yang sifatnya sangat mirip atau sifatnya sangat luas sehingga konflik ini mungkin terjadi? Yaitu saya baik memilih lodash atau garis bawah tidak keduanya. Libraray landscape kami saat ini di javascript terlalu jenuh dan pengembang (secara umum) menjadi sangat ceroboh dalam menambahkannya sehingga kami akhirnya menghindari praktik terbaik untuk menenangkan Lib Lords kami
micahblu
2
@micahblu - perpustakaan dapat memutuskan untuk memodifikasi objek standar hanya untuk kenyamanan pemrograman internalnya sendiri (itulah sebabnya mengapa orang ingin melakukan ini). Jadi, konflik ini tidak dihindari hanya karena Anda tidak akan menggunakan dua perpustakaan yang memiliki fungsi yang sama.
jfriend00
1
Anda juga dapat (pada es2015) membuat kelas baru yang memperluas kelas yang ada, dan menggunakannya dalam kode Anda sendiri. jadi jika MyError extends Errordapat memiliki stringifymetode tanpa bertabrakan dengan subclass lainnya. Anda masih harus menangani kesalahan yang tidak dihasilkan oleh kode Anda sendiri, sehingga mungkin kurang bermanfaat Errordibandingkan dengan yang lain.
ShadSterling
16

Menurut pendapat saya, ini adalah praktik yang buruk. Alasan utamanya adalah integrasi. Mengutip dokumen should.js:

OMG IT MEMPERPANJANG OBYEK ???!?! @ Ya, ya, dengan satu pengambil harus, dan tidak itu tidak akan merusak kode Anda

Nah, bagaimana penulis bisa tahu? Bagaimana jika kerangka kerja mengejek saya melakukan hal yang sama? Bagaimana jika janji saya melakukan hal yang sama?

Jika Anda melakukannya di proyek Anda sendiri maka tidak apa-apa. Tapi untuk perpustakaan, maka itu desain yang buruk. Underscore.js adalah contoh dari hal yang dilakukan dengan cara yang benar:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Stefan
sumber
2
Saya yakin respons TJ adalah tidak menggunakan janji-janji atau kerangka kerja yang mengejek itu: x
Jim Schubert
The _(arr).flatten()contoh sebenarnya meyakinkan saya untuk tidak memperpanjang objek asli. Alasan pribadi saya untuk melakukannya adalah murni sintaksis. Tapi ini memuaskan perasaan estetika saya :) Bahkan menggunakan beberapa nama fungsi yang lebih biasa ingin foo(native).coolStuff()mengubahnya menjadi beberapa objek "extended" tampak hebat secara sintaksis. Jadi terima kasih untuk itu!
egst
16

Jika Anda melihatnya berdasarkan kasus per kasus, mungkin beberapa implementasi dapat diterima.

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

Menimpa metode yang sudah dibuat menciptakan lebih banyak masalah daripada yang dipecahkan, itulah sebabnya mengapa hal itu dinyatakan secara umum, dalam banyak bahasa pemrograman, untuk menghindari praktik ini. Bagaimana Devs mengetahui fungsi telah diubah?

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

Dalam hal ini kami tidak menimpa metode JS core yang diketahui, tetapi kami memperluas String. Satu argumen dalam posting ini menyebutkan bagaimana pengembang baru mengetahui apakah metode ini adalah bagian dari inti JS, atau di mana menemukan dokumen? Apa yang akan terjadi jika objek JS String inti adalah untuk mendapatkan metode bernama capitalize ?

Bagaimana jika alih-alih menambahkan nama yang mungkin bertabrakan dengan perpustakaan lain, Anda menggunakan pengubah khusus perusahaan / aplikasi yang dapat dipahami semua devs?

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.

Jika Anda terus memperluas objek lain, semua devs akan menemui mereka di alam liar dengan (dalam hal ini) kami , yang akan memberi tahu mereka bahwa itu adalah ekstensi khusus perusahaan / aplikasi.

Ini tidak menghilangkan tabrakan nama, tetapi mengurangi kemungkinan. Jika Anda menentukan bahwa memperluas objek JS inti adalah untuk Anda dan / atau tim Anda, mungkin ini untuk Anda.

SoEzPz
sumber
7

Memperluas prototipe built-in memang ide yang buruk. Namun, ES2015 memperkenalkan teknik baru yang dapat digunakan untuk mendapatkan perilaku yang diinginkan:

Memanfaatkan WeakMaps untuk mengaitkan tipe dengan prototipe bawaan

Implementasi berikut memperluas Numberdan membuat Arrayprototipe tanpa menyentuhnya sama sekali:

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

Seperti yang Anda lihat, prototipe primitif pun dapat diperluas dengan teknik ini. Ini menggunakan struktur peta danObject identitas untuk mengaitkan tipe dengan prototipe bawaan.

Contoh saya mengaktifkan reducefungsi yang hanya mengharapkan Arrayargumen tunggal, karena dapat mengekstrak informasi cara membuat akumulator kosong dan cara menggabungkan elemen dengan akumulator ini dari elemen Array itu sendiri.

Harap dicatat bahwa saya bisa menggunakan Maptipe normal , karena referensi yang lemah tidak masuk akal ketika mereka hanya mewakili prototipe built-in, yang tidak pernah mengumpulkan sampah. Namun, a WeakMaptidak dapat diubah dan tidak dapat diperiksa kecuali Anda memiliki kunci yang tepat. Ini adalah fitur yang diinginkan, karena saya ingin menghindari segala bentuk refleksi.


sumber
Ini adalah penggunaan WeakMap yang keren. Hati-hati dengan itu xs[0]pada array kosong tho. type(undefined).monoid ...
Terima kasih
@naomik saya tahu - lihat pertanyaan terakhir saya , potongan kode ke-2.
Bagaimana standar dukungan untuk pelaku ini?
onassar
5
-1; apa gunanya semua ini? Anda hanya membuat jenis pemetaan kamus global terpisah untuk metode, dan kemudian secara eksplisit mencari metode dalam kamus berdasarkan jenis. Sebagai akibatnya, Anda kehilangan dukungan untuk pewarisan (metode "ditambahkan" dengan Object.prototypecara ini tidak dapat dipanggil pada a Array), dan sintaks secara signifikan lebih lama / lebih buruk daripada jika Anda benar-benar memperpanjang prototipe. Hampir selalu lebih mudah untuk hanya membuat kelas utilitas dengan metode statis; satu-satunya keuntungan pendekatan ini adalah dukungan terbatas untuk polimorfisme.
Mark Amery
4
@MarkAmery Hei teman, Anda tidak mengerti. Saya tidak kehilangan Warisan tetapi menyingkirkannya. Warisannya sudah 80-an, Anda harus melupakannya. Inti dari peretasan ini adalah untuk meniru kelas tipe, yang, secara sederhana, merupakan fungsi yang kelebihan beban. Namun ada kritik yang sah: Apakah berguna untuk meniru kelas tipe dalam Javascript? Tidak. Alih-alih menggunakan bahasa yang diketik yang mendukung mereka secara asli. Saya cukup yakin inilah yang Anda maksudkan.
7

Satu lagi alasan mengapa Anda tidak perlu memperluas Objek asli:

Kami menggunakan Magento yang menggunakan prototype.js dan memperluas banyak hal pada Object asli. Ini berfungsi dengan baik sampai Anda memutuskan untuk mendapatkan fitur baru dan di situlah masalah besar dimulai.

Kami telah memperkenalkan komponen Web di salah satu halaman kami, jadi webcomponents-lite.js memutuskan untuk mengganti seluruh Objek Acara (asli) di IE (mengapa?). Ini tentu saja memecah prototype.js yang pada gilirannya memecah Magento. (sampai Anda menemukan masalah, Anda dapat menginvestasikan banyak jam untuk melacaknya kembali)

Jika Anda suka masalah, terus lakukan!

Eugen Wesseloh
sumber
4

Saya dapat melihat tiga alasan untuk tidak melakukan ini (dari dalam aplikasi , setidaknya), hanya dua di antaranya yang dibahas dalam jawaban yang ada di sini:

  1. Jika Anda salah melakukannya, Anda akan secara tidak sengaja menambahkan properti enumerable ke semua objek dari tipe yang diperluas. Mudah dikerjakan menggunakanObject.defineProperty , yang menciptakan properti non-enumerable secara default.
  2. Anda mungkin menyebabkan konflik dengan perpustakaan yang Anda gunakan. Dapat dihindari dengan tekun; cukup periksa metode apa yang didefinisikan oleh perpustakaan yang Anda gunakan sebelum menambahkan sesuatu ke prototipe, periksa catatan rilis saat meningkatkan, dan uji aplikasi Anda.
  3. Anda mungkin menyebabkan konflik dengan versi masa depan dari lingkungan JavaScript asli.

Poin 3 bisa dibilang yang paling penting. Anda dapat memastikan, melalui pengujian, bahwa ekstensi prototipe Anda tidak menyebabkan konflik dengan perpustakaan yang Anda gunakan, karena Anda memutuskan perpustakaan apa yang Anda gunakan. Hal yang sama tidak berlaku untuk objek asli, dengan asumsi kode Anda berjalan di browser. Jika Anda menentukan Array.prototype.swizzle(foo, bar)hari ini, dan besok Google menambahkan Array.prototype.swizzle(bar, foo)ke Chrome, Anda mungkin berakhir dengan beberapa rekan bingung yang bertanya-tanya mengapa.swizzle perilaku tampaknya tidak cocok dengan apa yang didokumentasikan di MDN.

(Lihat juga kisah bagaimana mengutak-atik mootool dengan prototipe yang tidak mereka miliki memaksa metode ES6 diganti namanya untuk menghindari melanggar web .)

Ini dapat dihindari dengan menggunakan awalan khusus aplikasi untuk metode yang ditambahkan ke objek asli (misalnya mendefinisikan Array.prototype.myappSwizzlebukan Array.prototype.swizzle), tapi itu agak jelek; itu hanya dapat dipecahkan dengan menggunakan fungsi utilitas mandiri alih-alih menambah prototipe.

Mark Amery
sumber
2

Perf juga merupakan alasan. Terkadang Anda mungkin perlu mengulang tombol. Ada beberapa cara untuk melakukan ini

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

Saya biasanya menggunakan for of Object.keys()karena melakukan hal yang benar dan relatif singkat, tidak perlu menambahkan cek.

Tapi, ini jauh lebih lambat .

for-of vs for-in hasil perf

Hanya menebak alasannya Object.keyslambat sudah jelas, Object.keys()harus membuat alokasi. Bahkan AFAIK harus mengalokasikan salinan semua kunci sejak itu.

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

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

Mungkin saja mesin JS dapat menggunakan beberapa jenis struktur kunci yang tidak dapat diubah sehingga Object.keys(object)mengembalikan sesuatu referensi yang beralih pada kunci yang tidak dapat diubah dan yang object.newPropmenciptakan objek kunci yang sama sekali tidak dapat diubah baru tapi apa pun, itu jelas lebih lambat hingga 15x

Bahkan memeriksa hasOwnPropertyhingga 2x lebih lambat.

Inti dari semua itu adalah bahwa jika Anda memiliki kode sensitif perf dan perlu untuk mengulang kunci maka Anda ingin dapat menggunakan for intanpa harus menelepon hasOwnProperty. Anda hanya dapat melakukan ini jika Anda belum mengubahObject.prototype

Perhatikan bahwa jika Anda menggunakan Object.definePropertyuntuk memodifikasi prototipe jika hal-hal yang Anda tambahkan tidak dapat dihitung maka mereka tidak akan mempengaruhi perilaku JavaScript dalam kasus-kasus di atas. Sayangnya, setidaknya di Chrome 83, mereka mempengaruhi kinerja.

masukkan deskripsi gambar di sini

Saya menambahkan 3000 properti non-enumerable hanya untuk mencoba memaksa masalah perf muncul. Dengan hanya 30 properti tes terlalu dekat untuk mengetahui apakah ada dampak perf.

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

Firefox 77 dan Safari 13.1 tidak menunjukkan perbedaan dalam perf antara kelas Augmented dan Unaugmented mungkin v8 akan diperbaiki di area ini dan Anda dapat mengabaikan masalah perf.

Tapi, izinkan saya juga menambahkan ada kisahnyaArray.prototype.smoosh . Versi singkatnya adalah Mootools, perpustakaan populer, dibuat sendiri Array.prototype.flatten. Ketika komite standar mencoba untuk menambahkan yang asli Array.prototype.flattenmereka menemukan tidak bisa tanpa melanggar banyak situs. Para devs yang mengetahui tentang istirahat menyarankan menamai metode es5 smooshsebagai lelucon tetapi orang-orang panik tidak mengerti itu adalah lelucon. Mereka memilih flatbukannyaflatten

Moral dari cerita ini adalah Anda tidak harus memperluas objek asli. Jika Anda melakukannya, Anda bisa mengalami masalah yang sama tentang kerusakan barang-barang Anda dan kecuali perpustakaan khusus Anda menjadi sepopuler MooTools, vendor peramban tidak mungkin mengatasi masalah yang Anda sebabkan. Jika pustaka Anda menjadi sepopuler itu, itu akan menjadi semacam cara untuk memaksa orang lain mengatasi masalah yang Anda sebabkan. Jadi, tolong jangan Perluas Objek Asli

gman
sumber
Tunggu, hasOwnPropertymemberi tahu Anda apakah properti ada di objek atau prototipe. Tetapi for inloop hanya memberi Anda properti enumerable. Saya baru saja mencoba ini: Tambahkan properti non-enumerable ke prototipe Array dan itu tidak akan muncul dalam loop. Tidak perlu untuk tidak memodifikasi prototipe agar loop paling sederhana bekerja.
ygoe
Tapi itu masih praktik yang buruk karena alasan lain;)
GM