Memikirkan kembali deepClone

Seperti yang Anda ketahui dalam JavaScript, objek disalin dengan referensi. Tapi kadang-kadang Anda perlu melakukan kloning objek secara mendalam. Banyak perpustakaan js menawarkan implementasi fungsi deepClone untuk kasus ini. Namun, sayangnya, sebagian besar perpustakaan tidak memperhitungkan beberapa hal penting:

  • Array mungkin terletak pada objek dan lebih baik untuk menyalinnya sebagai array
  • Objek mungkin memiliki bidang dengan simbol sebagai kunci
  • Bidang objek memiliki deskriptor selain default
  • Fungsinya bisa di bidang objek dan juga harus dikloning.
  • Suatu objek akhirnya memiliki prototipe yang berbeda dari Object.prototype

Siapa yang memecahkannya, saya meletakkan kode lengkap di bawah spoiler
function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } function clonePrimitive(source) { return () => source; } function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

Implementasi saya ditulis dalam gaya fungsional yang memberi saya keandalan, stabilitas, dan kesederhanaan. Tetapi karena, sayangnya, masih banyak yang tidak dapat membangun kembali pemikiran mereka dengan proseduralisme dan pseudo-OOP, saya akan menjelaskan setiap batu bata implementasi saya:

Fungsi deepClone itu sendiri akan mengambil 1 sumber argumen - sumber dari mana kami akan mengkloning, dan klon dalamnya dengan semua fitur di atas akan dikembalikan:

 function deepClone(source) { return ({ 'object': cloneObject, 'function': cloneFunction }[typeof source] || clonePrimitive)(source)(); } 

Semuanya sederhana di sini, tergantung pada jenis data dalam sumber, fungsi dipilih yang dapat mengkloningnya, dan sumber itu sendiri ditransfer ke sana.

Anda juga dapat melihat bahwa hasil yang dikembalikan disebut sebagai fungsi tanpa parameter sebelum dikembalikan ke pengguna. Ini perlu, karena saya membungkus nilai yang saya clone, dalam functor paling sederhana, agar dapat bermutasi tanpa melanggar kemurnian fungsi bantu. Berikut ini adalah implementasi dari functor ini:

 function simpleFunctor(value) { return mapper => mapper ? simpleFunctor(mapper(value)) : value; } 

Dia dapat melakukan 2 hal - memetakan (jika fungsi mapper diteruskan kepadanya) dan mengekstrak (jika tidak ada yang berlalu).

Sekarang kita akan menganalisis fungsi bantu cloneObject, cloneFunction dan clonePrimitive. Masing-masing mengambil 1 argumen sumber dari tipe tertentu dan mengembalikan klonnya.

Implementasi cloneObject harus mempertimbangkan bahwa array juga bertipe objek, yah, dalam kasus lain, mereka harus mengkloning field dan prototipe. Berikut implementasinya:

 function cloneObject(source) { return (Array.isArray(source) ? () => source.map(deepClone) : clonePrototype(source, cloneFields(source, simpleFunctor({}))) ); } 

Array dapat disalin menggunakan metode slice, tetapi karena kita memiliki kloning yang dalam dan array tidak hanya berisi nilai-nilai primitif, metode peta digunakan dengan deepClone yang dijelaskan di atas sebagai argumen.

Untuk objek lain, kami membuat objek baru dan membungkusnya dengan functor kami yang dijelaskan di atas, mengkloning bidang (bersama dengan deskriptor) menggunakan fungsi helper cloneFields, dan kemudian mengkloning prototipe menggunakan clonePrototype.

Fungsi pembantu akan saya jelaskan di bawah ini. Sementara itu, pertimbangkan implementasi cloneFunction :

 function cloneFunction(source) { return cloneFields(source, simpleFunctor(function() { return source.apply(this, arguments); })); } 

Anda tidak bisa mengkloning suatu fungsi dengan semua logika. Tetapi Anda dapat membungkusnya dalam fungsi lain yang memanggil sumber asli dengan semua argumen dan konteks, dan mengembalikan hasilnya. "Klon" seperti itu tentu akan menjaga fungsi asli dalam memori, tetapi akan "menimbang" sedikit dan sepenuhnya mereproduksi logika asli. Kami membungkus fungsi yang dikloning dalam functor dan menggunakan cloneFields kami menyalin semua bidang dari fungsi asli ke dalamnya, karena fungsi dalam JS juga merupakan objek, baru saja dipanggil, dan karenanya dapat menyimpan bidang di dalamnya.

Secara potensial, suatu fungsi mungkin memiliki prototipe yang berbeda dari Function.prototype, tapi saya tidak mempertimbangkan kasus ekstrem ini. Salah satu pesona FP adalah kita dapat dengan mudah menambahkan pembungkus baru pada fungsi yang ada untuk mengimplementasikan fungsionalitas yang diperlukan.

Bata bangunan clonePrimitive terakhir berfungsi untuk mengkloning nilai-nilai primitif. Tetapi karena nilai primitif disalin oleh nilai (atau dengan referensi, tetapi tidak dapat diubah dalam beberapa implementasi mesin JS), kita dapat menyalinnya. Tetapi karena kita tidak diharapkan untuk mendapatkan nilai murni, tetapi nilai yang dibungkus dengan functor yang diekstraksi dapat memanggil tanpa argumen, kita akan membungkus nilai kita dalam suatu fungsi:

 function clonePrimitive(source) { return () => source; } 

Sekarang kita mengimplementasikan fungsi bantu yang digunakan di atas - clonePrototype dan cloneFields

Untuk mengkloning prototipe, clonePrototype hanya akan mengekstrak prototipe dari objek sumber dan, dengan melakukan operasi peta pada functor yang dihasilkan, atur ke objek target:

 function clonePrototype(source, destinationFunctor) { return destinationFunctor(destination => Object.setPrototypeOf(destination, Object.getPrototypeOf(source))); } 

Bidang kloning sedikit lebih rumit, jadi saya membagi fungsi cloneFields menjadi dua. Fungsi eksternal mengambil rangkaian semua bidang bernama dan semua bidang simbol, menerima sepenuhnya semua bidang, dan menjalankannya melalui peredam yang dibuat oleh fungsi bantu:

 function cloneFields(source, destinationFunctor) { return (Object.getOwnPropertyNames(source) .concat(Object.getOwnPropertySymbols(source)) .reduce(makeCloneFieldReducer(source), destinationFunctor) ); } 

makeCloneFieldReducer harus membuat fungsi reducer untuk kita yang dapat diteruskan ke metode pengurangan pada array semua bidang objek sumber. Sebagai baterai, fungsi kami yang menyimpan target akan digunakan. Peredam harus mengekstrak pegangan dari bidang objek sumber dan menetapkannya ke bidang objek target. Tetapi di sini penting untuk mempertimbangkan bahwa ada dua jenis deskriptor - dengan nilai dan dengan get / set. Jelas, nilai perlu dikloning, tetapi dengan get / set tidak ada kebutuhan seperti itu, deskriptor seperti itu dapat dikembalikan seperti:

 function makeCloneFieldReducer(source) { return (destinationFunctor, field) => { const descriptor = Object.getOwnPropertyDescriptor(source, field); return destinationFunctor(destination => Object.defineProperty(destination, field, 'value' in descriptor ? { ...descriptor, value: deepClone(descriptor.value) } : descriptor)); }; } 

Itu saja. Implementasi deepClone seperti itu memecahkan semua masalah yang ditimbulkan pada awal artikel. Selain itu, ini dibangun di atas fungsi murni dan satu functor, yang memberikan semua jaminan yang melekat dalam kalkulus lambda.

Saya juga mencatat bahwa saya tidak menerapkan perilaku yang sangat baik untuk koleksi selain array yang akan layak dikloning secara individual, seperti Peta atau Set. Meskipun dalam beberapa kasus ini mungkin diperlukan.

Source: https://habr.com/ru/post/id466535/


All Articles