😎Shallow copy, Deep copy라고도한다.
쉽게 설명하면 얕은 복사는 depth 1까지만 복사를 수행해주는 것
깊은 복사는 depth 2이상도 잘 복사해주는 것입니다.
얕은 복사는 쉽게 수행할 수 있지만.
깊은 복사는 꽤 많은 에러핸들링을 곁들인 재귀코드를 작성해야 정상적으로 수행할 수 있습니다.
물론 그런 귀찮은 점 때문에 lodash , immer 등
깊은 복사를 도와주는 수많은 라이브러리가 존재합니다.
하지만 이번에는 어떤 방식으로 deep copy가 되는지 한번 코드로 작성해보는 시간을 가지겠습니다.
😎얕은 복사하기
아주 간단합니다. 얕은 복사는 매우 자주 사용하게되고..
우리도 쉽게 접근할 수 있습니다.
객체에는 Object.assign()
배열에는 slice(),map(),filter() 등등이 얕은 복사를 구현하는 메서드들입니다.
이외에도 ... 스프레드 연산자를 이용하여 얕은복사를 구현할 수도 있습니다.
😎 재귀 함수를 통해 직접 deep clone을 구현해보자.
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj; // Return non-object or null values as-is
}
let result;
// Handle Arrays
if (Array.isArray(obj)) {
result = [];
for (let i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i]);
}
}
// Handle Objects
else {
result = {};
for (let key in obj) {
result[key] = deepCopy(obj[key]);
}
}
return result;
}
let deepobj = {
hi: [1, 2, 3, 4, 5],
hey: '1234',
um: 1234,
firstName: 'John',
lastName: 'Doe',
};
let a = deepCopy(deepobj);
console.log(deepobj); // { hi: [ 1, 2, 3, 4, 5 ], hey: '1234', um: 1234 }
console.log(a); // { hi: [ 1, 2, 3, 4, 5 ], hey: '1234', um: 1234 }
console.log(a.hi === deepobj.hi); //false
console.log(deepobj === a); //false
console.log(Array.isArray(a.hi)); // true
제법 잘 동작하는 것 같이 보입니다.
만약 Array가 value인 경우의 핸들링도 충실히 구현한 모습입니다.
복사는 잘 이루어져 밸류를 array로 가지는
a.hi 와 deepobj.hi의 비교결과는 false로 나오는 모습
하지만 Array.isArray(a.hi)를 해보면 정상적으로 true가 출력되는 모습입니다.
당연한거 아니냐 생각을 할 수 있지만...
핸들링해주지 않고 작성하면 내 배열이 오브젝트가 되어버립니다.
그런데 아직 완벽하지는 않습니다.
😎getter와 setter가...죽었어요
let deepobj = {
hi: [1, 2, 3, 4, 5],
hey: '1234',
um: 1234,
firstName: 'John',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ');
},
};
아까 코드에서 간단한 getter , setter 프로퍼티를 설정해줬습니다.
deepobj.fullName에 인자로 문자열을 넘겨주면
그 문자열을 공백을 기준으로 split해서 디스트럭처링 할당해주는 간단한 코드입니다.
예를 들어 문득 "내이름은 엄준식"으로 이름을 바꿔주고싶어서
deepobj.fullName = "내이름은 엄준식"을 넣어줬을때
setter 함수는 firstName에 "내이름은" 을 lastName에 "엄준식"을 할당해주는 것입니다.
실제로 잘 작동하는지 한번 테스트해보겠습니다.
deepobj.fullName = '제이름은 엄준식';
console.log(deepobj);
{
hi: [ 1, 2, 3, 4, 5 ],
hey: '1234',
um: 1234,
firstName: '제이름은',
lastName: '엄준식',
fullName: [Getter/Setter]
}
아주 멋지게 작동하네요
이제 deepobj를 deep copy한 a 변수에도 실행을 시켜주고싶습니다.
한번 해보겠습니다.
a.fullName = '저는 왜못하죠';
console.log(a);
{
hi: [ 1, 2, 3, 4, 5 ],
hey: '1234',
um: 1234,
firstName: 'John',
lastName: 'Doe',
fullName: '저는 왜못하죠'
}
엄....
firstName과 lastName에 입력값을 분배해주는게 아니라
그냥 fullName에 그대로 문자열이 들어가버렸습니다.
명백히 getter,setter함수가 소멸한 모습이라고 볼 수 있습니다.
😎그냥 함수는 괜찮을까요?
let deepobj = {
hi: [1, 2, 3, 4, 5],
hey: '1234',
um: 1234,
firstName: 'John',
lastName: 'Doe',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ');
},
hey() {
console.log('hey you');
},
};
console.log(deepobj.hey()); // "hey you"
console.log(a.hey()); // "hey you"
console.log(typeof deepobj.hey); // function
console.log(typeof a.hey); // function
네.. 그냥 함수는 괜찮습니다.좋아요.. getter setter만 해결하면 되겠군요
😎 딥클론최종진짜최종찐최종찐막.pdf
function deepCopy(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
return obj; // Return non-object or null values as-is
}
if (hash.has(obj)) {
return hash.get(obj); // If the object has already been encountered, return it
}
let result;
// Handle Arrays
if (Array.isArray(obj)) {
result = [];
hash.set(obj, result); // Add the new array to the hash table
for (let i = 0; i < obj.length; i++) {
result[i] = deepCopy(obj[i], hash);
}
}
// Handle Objects
else {
result = Object.create(obj.constructor.prototype); // Create a new object with the same prototype as the original
hash.set(obj, result); // Add the new object to the hash table
for (let key in obj) {
let descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor.get || descriptor.set) {
Object.defineProperty(result, key, descriptor); // If the property has a getter or setter, use defineProperty to copy it
} else {
result[key] = deepCopy(obj[key], hash);
}
}
}
return result;
}
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
제가 쓴 코드는 아니고 그냥 챗지피티를 갈궈서 얻었습니다.
가끔 헛소릴 할 때도 있지만.. 뭐 잘 돌아가는지 확인만 되면 장땡 아니겠습니까
고작 getter,setter 구분 하는데 이만큼의 코드가 추가된다고..?? 싶을 수 있지만
어쩔수 없군요..
첫줄부터 굉장히 낯선 코드가 보입니다.
hash = new WeakMap() 이다.
WeakMap()이 뭐죠...? 하면 너무 어려우니까
키값이 Object여야만 하는 키밸류쌍이다.라고만 이해를 먼저 하겠습니다.
키값은 무적권 Object여야만 하지만 밸류는 any datatype 아무거나 와도 된다는 뜻이군요
물어보니 대충 이미 생성한 오브젝트를 또 생성하는건 비효율적이니
얼리리턴을 해주기 위해 사용해줬다고 하는데 뭐... 일단 넘어가겠습니다.
오브젝트 케이스를 핸들링할 때 눈에 띄는 점은
Object.create 메서드를 이용해 매개변수 obj의 생성자의 prototype을 result에 할당하는 것입니다.
그러고 나서 WeakMap에 obj를 키로 밸류는 새로 생성한 result 오브젝트를 할당하는군요
Object.getOwnPropertyDescriptor() 메서드를 사용하는데
위 메서드는 이렇게 동작합니다.
제 예제 코드를 가져와볼게요
console.log(Object.getOwnPropertyDescriptor(deepobj, 'fullName'));
console.log(Object.getOwnPropertyDescriptor(deepobj, 'hi'));
{
get: [Function: get fullName],
set: [Function: set fullName],
enumerable: true,
configurable: true
}
{
value: [ 1, 2, 3, 4, 5 ],
writable: true,
enumerable: true,
configurable: true
}
우리가 getter,setter프로퍼티로 정의해준 fullName을 인자로 넣었더니
get과 set이 담긴 객체가 나오고
반면 getter,setter가 아닌 프로퍼티를 넣어보니 value와 writable 가능여부가 튀어나오네요
아하..!만약에 getter나 setter함수가 있다면
result.get || result.set에서 undefined가 아닌 함수(트루시)값이 나올테니
그 경우에 getter, setter를 핸들링해주면 된다는 마인드
Object.defineProperty() 정적 메서드는 객체에 새로운 속성을 직접 정의하거나
이미 존재하는 속성을 수정한 후, 해당 객체를 반환합니다.
정의를 도와주는 메서드군요
MDN의 예제코드를 보면...
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// Throws an error in strict mode
console.log(object1.property1);
// Expected output: 42
wow.. 개인적으로 잘 이해가 안되는 동작이 있긴하지만 일단..okay..
console.log(object1); // {}
console.log(object1.hasOwnProperty('property1')); // true
너무 궁금해서 지피티한테 물어보니
console.log() method는 enmurable한 (열거가능한) 값만 보여주기 때문에
보이지 않는 것이라고 하네요
뭐 아무튼 객체에 새로운 속성을 정의했다...군요
그럼 만약에 enmurable에 대한 설정도 같이 주면 열거도 가능하겠네요?
const object1 = {};
let bs = Object.defineProperty(object1, 'property1', {
value: 42,
writable: false,
enumerable: true,
});
object1.property1 = 77;
// Throws an error in strict mode
console.log(object1.property1);
// Expected output: 42
console.log(bs.property1); 42
console.log(object1); //{ property1: 42 }
console.log(object1.hasOwnProperty('property1')); // true
아주 멋지군요
근데 definedProperty는 카피를 해주는 메서드가 아니라는데
그럼.. 저 코드에서 getter setter도 카피가 안된거아님?
원본 고치면 복사본도 영향받는거 아님?
Object.defineProperty()와 함께 deepCopy() 함수를 사용하여 객체의 깊은 복사본을 생성한 후 원본 객체의 getter 또는 setter 함수를 수정하면 생성된 복사본에 영향을 미치지 않습니다.
그 이유는 Object.defineProperty()가 함수 자체를 복사하는 대신
getter 또는 setter 함수에 대한 참조를 포함하는 새로운 속성 설명자 객체를 생성하기 때문입니다.
deepCopy() 함수가 Object.defineProperty()를 사용하여 getter 또는 setter로 속성을 복사할 때
실제로는 동일한 getter 또는 setter 함수를 원래 속성으로 참조하는 새 속성 설명자를 생성합니다.
이는 getter 또는 setter 함수가 원본 개체와 복사본 간에 공유되며 함수에 대한
모든 변경 사항이 두 개체에 모두 반영됨을 의미합니다.
그러나 getter 또는 setter 함수를 새 함수 개체로 바꾸면 복사본은 여전히
원래 함수 개체를 참조하며 변경 사항의 영향을 받지 않습니다.
따라서 개체의 전체 복사본에서 속성의 동작을 수정하려면
기존 속성을 수정하는 대신 새 getter 또는 setter 함수를 사용하여 새 속성을 정의해야 합니다.
okay... 아니라고요? 네...
장문이 읽기 힘들면 형광펜 칠해놓은 부분만 읽어보면 되겠습니다.
그럼 잘 동작하는지나 보죠... 좋아요... 이제 슬슬 힘드니까요.
😎그래서 잘 되나요?
let a = deepCopy(deepobj);
a.fullName = '이제 되네요';
{
hi: [ 1, 2, 3, 4, 5 ],
hey: [Function: hey],
um: 1234,
firstName: 'John',
lastName: 'Doe',
fullName: [Getter/Setter]
}
{
hi: [ 1, 2, 3, 4, 5 ],
hey: [Function: hey],
um: 1234,
firstName: '이제',
lastName: '되네요',
fullName: [Getter/Setter]
}
와우 정말 멋집니다.
이제 왜 그냥 라이브러리 갖다 써야하는지 잘 알게되었어요!
고마워요 라이브러리!! 고마워요 챗지피티!!
'javascript' 카테고리의 다른 글
이터러블과 이터레이터를 이해하는 방법 (0) | 2023.03.06 |
---|---|
너무 재미있는 예제로 알아보는 실행컨텍스트 (0) | 2023.03.05 |
프로토타입을 케이크처럼 쉽게 먹는 방법 (3) | 2023.02.28 |
v8 엔진으로의 딥다이브 (0) | 2023.02.27 |
자바스크립트의 모듈을 내가 만들어서 써보자! (1) | 2023.02.24 |