معید خوش باطن
معید خوش باطن
خواندن ۱۳ دقیقه·۱ ماه پیش

انواع کپی در جاوا اسکریپت

سلام سلام
امیدوارم سرحال و پر انرژی باشید
اینجا خبری از لحن رسمی و کتابی نیست و قراره بشینیم کف زمین باهم صحبت کنیم.

خوب بیایید باهم اولش یه سری چیزا رو که برای دونستن deepCopy و shallowCopy نیاز داریم مرور کنیم و بعدش یاد بگیریم که deepCopy و shallowCopy تو جاوا اسکریپت چی هستنو چطور میتونیم ایجادشون کنیم و شاید بریم سراغ این که کجا باید از هرکدوم استفاده کنیم.


انواع تایپ ها تو جاوا اسکریپت

اول از همه بیایید ببینیم تو جاوا اسکریپت چند نوع تایپ داریم:( اگر بلدی این تیکه رو رد کن که زمان مهم ترین گنج برای همه‌ست!)

در کل ما ۸ نوع تایپ داریم که خود اونا به دو دسته کلی PrimitiveType و ReferenceType تقسیم میشن که جلوتر مفصل تفاوتشونو متوجه میشیم، اما اون 8 تا تایپ چیان؟

PrimitiveType:

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Symbol
  • BigInt

ReferenceType:

  • Object

همینجا یه نکته مهمی رو بگم: این آبجکتی که اینجا نوشته شده در اصل می‌تونه شامل ساختارهایی مثل آبجکت‌ها، آرایه ها، تاریخ یا ... باشه.

اولین بخش PrimitiveType

در کل میتونیم بگیم که Primitive ها immutable (ترجمه فارسیشم تغییر ناپذیر ) هستن و در عین حال هیچ پراپرتی یا متدی ندارن.

اولین سوال‌تون این میتونه باشه که در لحظه برای استرینگ ها یه سری متد داریم مثل split. مگه نمیگی متد نداره؟! پس این چیه؟

خوب باید بگم بهتون که جاوا اسکریپت بزرگوار primitive string رو به object string تبدیل میکنه و اینطوری میشه که متد داریم براشون.

بزارید یه مثال بزنم براتون فرض کنید من یه متغیر دارم که به عنوان مقدارش یه استرینگ مثل زیر داره:

const name = 'Moeid' name[0] = 'L'; console.log(name); // output: 'Moeid'

اینجا چون string ها از نوع primtive هستن تغییری تو مقدار متغیر name نمیبینیم و دوباره Moeid چاپ میشه.

بزارید یکم دقیق‌تر بشیم در واقع اینجا اگر از useStrict استفاده کنید میبینید که یه خطای زیبا بهتون میده که میگه:

Uncaught TypeError: Can not assign to read-only property '0' of string 'Moeid'


دومین بخش ReferenceType

حالا نوبتی هم باشه نوبت ReferenceType هاست که میتونیم بگیم mutable هستن و به راحتی میتونید مقادیر پراپرتی یا متدهای داخلیشونو تغییر بدید

فرض کنید من یه دوستی دارم به اسم "چنگیز" و این چنگیز قصه ما دوتا بچه داره، پس من یه متغیر دارم برای نگهداری اطلاعات بچه‌هاش:

const changizChildrenInformation = [ { name: 'Gholi', age: 5 }, { name: 'Taha', age: 3 } ];


خوب تو این مثال اطلاعات بچه های چنگیز یه آرایه ای از آبجکت‌هاست که من میتونم تغییرش بدم و ثابت نیست مثلا فرض کنید یه سال گذشت و بچه های چنگیز یه سال بزرگتر شدن:

changizChildrenInformation[0].age = 6; changizChilrenInformantion[1].age = 4;

الان به راحتی سن قلی و طاها تغییر کرد، در صورتیکه رفرنس آرایه تغییر نمیکنه.

به نظرم همینقدر کافیه ولی اگر دوست دارید بیشتر بریم تو دل اینکه دقیقا رفرنس تایپ ها و Primitive تایپ‌ها چه تفاوتی دارن تو نظرات برام بنویسید تا یکم بیشتر صحبت کنیم تو یه مطلب دیگه :)

بریم سر بحث شیرین کپی 😁


کپی در PrimitiveType:

تو این نوع، کپی کردن به شدت ساده ست و کافیه به صورت زیر عمل کنیم.

const num1 = 10; const num2 = num1;

به همین راحتی مقدار num1 در مقدار num2 ریخته شد و اینها مستقل از هم هستن، یعنی اگر num1 رو دوتا اضافه کنم num2 تغییری نمیکنه.


اما سوال مهم آیا در referenceType ها هم به همین صورته؟

بزارید با مثال جلوتر بریم که ملموس تر باشه، فرض کنید به جای یه استرینگ ما یه آبجکت داشته باشیم:

const user = { name: 'Changiz', age: 50 }; const _user = user; console.log( user ); console.log( _user );
// output: { name : 'Changiz', age: 50 } // output: { name : 'Changiz', age: 50 }

اولش میبینیم که نتیجه مثل بالاست و خوشحال میشیم اما بیایید یه بلایی سر آبجکت اول بیاریم:

const user = { name: 'Changiz', age: 50 } const _user = user; user.age = 'i don't know'; console.log( user ); console.log( _user );
// output: { name : 'i don't know', age: 50 } // output: { name : 'i don't know', age: 50 }

با این کار میبینیم ای بابا آبجکت دوم هم تغییر کرد. چرا ؟ ( پایه اید یه ذره بیشتر خرد ماجرا بشیم؟! )

بیایید برگردیم به مطالب پایه در علوم کامپیوتر.

میدونیم که زبان های برنامه نویسی دو نوع فضا برای ذخیره سازی داده در مموری کامپیوتر شما دارن یکی stack (یا فارسیش میشه پشته) و نوع دیگه heap هست.

استک چیه؟ هیپ چیه؟ اینجا کجاست؟


استک و هیپ:

استک: یه فضای ذخیره سازی موقته که در اون صرفا داده های primitive و پوینتر هایی که به ساختار های referenceType اشاره میکنن ذخیره میشن.

هیپ: در هیپ صرفا متغیر‌های گلوبال و آبجکت ها ذخیره میشن. ( بازم تاکید کنم که آبجکت ها تو هیپ ذخیره میشن و پوینترهایی که به اینا اشاره میکنه تو استک ذخیره میشن)

این شکل یه نمایی از هیپ و استکه که تمامی مثال های قبلی رو تو خودش جا داده
این شکل یه نمایی از هیپ و استکه که تمامی مثال های قبلی رو تو خودش جا داده


خوب الان متوجه میشیم که چرا با تغییر یکی از آبجکت ها اون یکی هم تغییر میکنه. چون که دو تا پوینتر در استک ایجاد شده که به یه آبجکت تو هیپ اشاره میکنن و ما یه تغییر توی آبجکتی که سمت هیپ ذخیره شده دادیم!


DeepCopy & ShallowCopy:

اما بالاخره رسیدیم به این موضوع زیبا و جذاب

اگر بخواهیم از یه دید دیگه به آبجکت ها نگاه کنیم ما دو نوع آبجکت داریم: flat و nested

نوع flat همیشه مقادیر داخلش primitve هستن مثلا:

const flat = [1,2,3];

اما nested ها مقادیر داخلشون non-primtiveعه مثلا:

const test = [ 'Hello', { firstName: 'Moeid', lastName: 'Khoshbaten'}, 123456789]

در کل shallowCopy برای آبجکت های نوع flat استفاده میشه و deepCopy برای nested.


انواع راه های ایجاد یه ShallowCopy:

۱. استفاده از spread syntax: یکی از قابلیت هایی که از es6 به بعد به جاوا اسکریپت اضافه شده spread syntax هست. از این بزرگوار میتونیم برای ایجاد یه shallowCopy به صورت زیر استفاده کنیم:

const user = { name: 'Gholi', age: 12, Email: 'gholi@gmail.com' }; const _user = { ...user } console.log( user ); console.log( _user );
// output: { name: 'Gholi', age: 12, Email: 'gholi@gmail.com' } // output: { name: 'Gholi', age: 12, Email: 'gholi@gmail.com' }

به همین راحتی شما یه shallowCopy انجام دادی و حالا با تغییر user، آبجکت user_ تغییری نمیکنه.

۲. استفاده از Object.assign:

این بزرگوار یه متد از آبجکت هاست که با استفاده ازش میتونید تمام پراپرتی های enumerable که ownProperty هم هستن از یه آبجکت کپی کنید تو یه آبجکت دیگه. ( اینکه تعریف enumerable چیه و ownPropery چیه واقعا مفصله و در این مقال نمیگنجه و اگر نمیدونید به نظرم خوبه که درباره اش بخونید ولی علی الحساب اینطوری در نظر بگیرید که این متد قراره پراپرتی های یه آبجکتو کپی کنه تو یه آبجکت دیگه :) این دو تا شرط جزو ریزه کاری هاست! )

بزارید یه مثال هدفدار از object.assign بزنم:

const userName = { name: 'Gholi', } const userAge = { age: 12 } const user = Object.assign(userName, userAge);
console.log('>>>userName: ', userName); console.log('>>>userAge: ', userAge); console.log('>>>user: ', user);

حالا خروجی به نظرتون چی هست؟

اینجا یکم جالب تر میشه نسبت به spread syntax، خروجی به صورت زیر میشه:

>>>userName: { name: 'Gholi', age: 12 } >>>userAge: { age: 12 } >>>user: { name: 'Gholi', age: 12 }


خوب چه اتفاقی افتاد مگه کپی نکرد؟! پس چرا اولی تغییر کرد؟

نکته اینجاست که ورودی اول، آبجکت هدفی هست که تمام پراپرتی ها قراره تو اون کپی بشن و نکته جالب تر اینه که object.assign چیزی که ریترن میکنه عملا رفرنس همون آبجکت هدفه و اگر شما در مثال بالا تغییری روی user بدید روی userName هم اعمال میشه.


خوب حالا اگر بخواهیم یه آبجکتو کپی بکنیم کافیه ورودی اول رو یه آبجکت خالی در نظر بگیریم:

const user = Object.assign({} , userName, userAge );

جزئیات بیشتر:

اگر بخواهیم یکم وارد جزئیات بیشتری بشیم یه تفاوت دیگه هم Objcet.assign نسبت به spread syntax داره اینه که Object.assign در حین کپی داره setterهای آبجکت هدف رو فراخوانی میکنه اما موقع استفاده از spread syntax، سترهای آبجکت هدف فراخوانی نمیشن!

یه تفاوت دیگه ای که Object.assign نسبت به spread syntax داره اینه که وقتی از Object.assign استفاده میکنیم عناصر جدید به انتهای آبجکت هدف push میشن ولی در استفاده از spread syntax جایگاه عناصر رو خودمون میتونیم تعیین کنیم.


۳. استفاده از Array.from :

این متد برای نوع های خاصی از آبجکت ها مورد استفاده قرار میگیره مثل Set ها و Map ها و آرایه ها.

در اصل Array.from یه روش static برای ساخت یه آرایه جدید از یک آبجکت iterable یا آرایه ماننده.

بزارید یه مثال برای Set بزنم:

const set = new Set([ 'hello', 'hi', 'salam', 'hola', 'hello']); const array = Array.from(set); console.log( set ); console.log( array );
//output: Set(4) { 'hello', 'hi', 'salam', 'hola' } //output: [ 'hello', 'hi', 'salam', 'hola' ]

نکته: اینو هم یادتون باشه که استفاده از Set ها باعث میشه که عناصر تکراری تو آرایه حذف بشن. تو مثال بالا hello دوبار در آرایه ای که به عنوان ورودی به Set داده شده تکرار شده اما اگر به لاگ ها دقت کنید فقط یک hello در اونها وجود داره.


۴. استفاده از Object.create:

این متد برای ساخت یک آبجکت از یک آیجکت دیگه استفاده میشه و نکته ای که داره اینه که در اصل کپی از آبجکت ایجاد نمیکنه و یه آبجکت میسازه که prototype اش به پروتوتایپ آبجکت اصلی اشاره میکنه و به عبارتی پراپرتی‌هایی که در اون هستن به نحوی از آبجکت اصلی دارن ارث بری میکنن و به خودی خود هیچی نداره! بیایید با یه مثال بهتر متوجه این موضوع بشیم:

const lang = { name: 'JavaScript', age: 12 }; const _lang = Object.create( lang ); console.log( lang ); console.log( _lang )
// output: { name: 'JavaScript', age: 12 } // output: {}

خوب سوال اول این چرا پس خالیه ؟!

نه، در اصل خالی نیست گفتم داره ارث بری میکنه و چیزایی که ارث بری میشن تو خروجی نمیان یعنی اگر لاگمو اینطوری کنم خروجی تغییر میکنه:

console.log( _lang.name ); console.log( _lang.age )
// output: 'JavaScript' // output: 12

پس در عمل اون پراپرتی ها، مال خودش نیستن بلکه یه ارثی هست که از پدرش بهش رسیده. اماااااا

حالا بیایید یه کاری کنیم، مثلا age رو تغییر بدیم ببینیم چی میشه:

_lang.age = 150; console.log( _lang );
// output: { age: 150 }

عع چیشد؟!! وقتی مقدار یک پراپرتی که ارث برده رو تغییر بدیم این پراپرتی به عنوان ارث دیگه دیده نمیشه بلکه میشه مال خودش، یه ویژگی خاص که فقط برای خودشه!

حالا سوال مهم!

اگر برای آبجکت های نوع nested از shallowCopy استفاده کنیم چه مشکلی ممکنه به وجود بیاد؟ با یه مثال میبینیم:

const user = [ 'Gholi' , {age: 15, country: 'Iran' } ]; const _user = [ ...user ]; console.log( user ); console.log( _user );
// output: [ 'Gholi' , {age: 15, country: 'Iran' } ] // output: [ 'Gholi' , {age: 15, country: 'Iran' } ]

همونطور که میبینید یه کپی ازش گرفته شده و ما باید خوشحال باشیم اما تو شکل پایین یه نمایش از استک و هیپ تو این مثال داشته باشیم:


درسته که تو لاگ هایی که دیدیم ظاهرا تغییری دیده نمیشه اما بیایید یه تغییر روی آرایه دومی بدیم:

_user[1].age = 126; console.log( user ); console.log( _user );
// output: [ 'Gholi' , {age: 126, country: 'Iran' } ] // output: [ 'Gholi' , {age: 126, country: 'Iran' } ]

ما اومدیم با استفاده از spread syntax یه کپی از user ایجاد کردیم اما به خاطر اینکه از نوع nested هست رفرنس آبجکت داخلی تغییری درش به وجود نمیاد پس اگر مقداری تغییر کنه تو آرایه اول هم این تاثیر رو میبینیم.

راه حل چیه؟

درود بر شما استفاده از deepCopy


انواع روش های deepCopy:

۱. استفاده از JSON.stringify و JSON.parst:

باهم یه مثال ببینیم:

const user = [ 'Gholi' , {age: 15, country: 'Iran' } ]; const _user = JSON.parse( JSON.stringify( user ) ); _user.age = 26; console.log( user ); console.log( _user );
// output: [ 'Gholi' , {age: 15, country: 'Iran' } ] // output: [ 'Gholi' , {age: 26, country: 'Iran' } ]

و یه deepCopy رو شاهدیم. حالا بزارید فرآیندی که طی میشه تا این deepCopy اتفاق میوفته رو ببینیم چیاست.

اول کار که با استفاده از JSON.stringify میایم آبجکتمونو به یه ساختار متنی با فرمت json تبدیل میکنیم و نکته مهم ماجرا دقیقا همینجاست فقط داده هایی اینجا ذخیره میشن که در فرمت json قابل نمایش باشن مثل اعداد، استرینگ ها، آرایه ها، آبجکت ها.

نکته مهم تو این بخش پوینتر هایی هست که به این آبجکت ها یا آبجکت هایی که به صورت nested در اون ها استفاده شدن اشاره میکنن. اون ها حذف میشن، همین باعث میشه که هیچ ارتباطی بین آبجکت اصلی و این ساختار متنی با فرمت json وجود نداشته باشه.


مرحله بعد استفاده از JSON.parse بود که با اون ساختار متنی JSON خونده میشه و یه آبجکت جدید ایجاد میشه. همونطور که گفتم چون ارتباطی بین این ساختار متنی و آبجکت اصلی وجود نداره، آبجکتی هم که از روی این ساختار متنی با استفاده از JSON.parse به وجود میاد هیچ ارتباطی با آبجکت اصلی نداره و همه مقادیر داخل آبجکت جدید تو حافظه ذخیره میشن.


مزایا و معایب:

مزایا:

۱. یه آبجکت کاملا مستقل ایجاد میشه

۲. برای داده های ساده مثل اعداد و استرینگ ها و آرایه ها و آبجکت های ساده مناسبه

معایب:

۱. از توابع پشتیبانی نمیکنه و این توابع تو مرحله JSON.stringify حذف میشن

۲. آبجکت های پیچیده مثل Date، Set، Map یا مقادیر undefined تو ساختار json ذخیره نمیشن

۳. اگر مقدار دیتایی که داریم زیاد باشه ممکنه کند باشه این روش، هر چند میشه با استفاده از یه سری کارا سرعت رو تا حدی بهبود بخشید ولی بازهم میتونیم این رو به عنوان یه عیب در نظر بگیریم


۲. استفاده از structuredClone:

یه متد قدرتمند که برای deepCopy از یه آبجکت که برخلاف روش قبلی میتونه با تایپ های پیچیده از آبجکت ها مثل Date، Set، Map ها یا حتی undefined یا Symbol ها کار کنه.

این متد از یه الگوریتم مشابه structured cloning تو جاوا اسکریپت استفاده میکنه و این در اصل همون تکنولوژی‌ای هست که برای کپی داده ها تو indexed db استفاده میشه.


نوبت مثال از این روشه بزارید با همون مثالی که برا روش قبل زدیم پیش بریم:

const user = [ 'Gholi' , {age: 15, country: 'Iran' } ]; const _user = structuredClone( user ); _user[1].age = 26; console.log( user ); console.log( _user );
// output: [ 'Gholi' , {age: 15, country: 'Iran' } ] // output: [ 'Gholi' , {age: 26, country: 'Iran' } ]

به همین راحتی یه deepCopy زیبا رو دیدیم.


مزایا و معایب:

مزایا:

۱. از داده های پیچیده پشتیبانی میکنه مثل تاریخ یا Set یا Map یا undefined یا Symbol ها

۲. بهینه تر و سریع تر از روش قبله


معایب:

۱. توابع همچنان کپی نمیشن

۲. آبجکت هایی که custom prototype هستن ممکنه پراپرتی هاشونو از دست بدن

۳. موقع استفاده از این روش باید حواستون به ساپورت مرورگر های مختلف باشه


۳. استفاده از کتابخونه ها:

یکی از روش های خوب هم میتونه استفاده از یه سری کتابخونه ها باشه که به ما کمک میکنن تا بتونیم یه deepCopy قوی داشته باشیم این زیر اسم چند تا از این کتابخونه ها رو میارم تا اگر دوست داشتید یه سری بهشون بزنید:

1. lodash

2. rfdc

3. immer

4. fast-copy

5. Ramda

6. clone-deep

7. deepcopy

8. klona


یه جدولی هم از مقایسه یه سری از این کتابخونه ها میزارم شاید تو انتخاب کمکتون کنه:


و درنهایت ممنونم که تا اینجا همراه من بودید و امیدوارم این مطلب براتون مفید بوده باشه.

تا یه مطلب دیگه بدرود👋🏻

جاوا اسکریپتکپیjavascriptfrontendbackend
software engineer | تقریبا دولوپر
شاید از این پست‌ها خوشتان بیاید