[JavaScript] 불변 객체, 얕은 복사, 깊은 복사

Mark·2022년 8월 9일
1
post-thumbnail

1. 불변객체

불변객체란 객체 내부의 프로퍼티들을 변경할 수 없도록 되어있는 객체, 값으로 전달받은 객체에 변경을 가하더라도 원본 객체는 변하지 않아야 하는 경우에 불변객체가 필요하다.

  • 참조형도 기본형과 마찬가지로 새로운 데이터를 할당할 때 기존 데이터는 변하지 않는다.
  • 내부 프로퍼티를 변경할 때 매번 새로운 객체를 만들어 재할당 하기로 규칙을 정하거나 자동으로 새로운 객체를 만드는 도구를 활용하면 객체 또한 불변성을 확보할 수 있다.

2. 불변 객체의 필요성

문제 상황

let person = {
    name : "tom",
    gender : "male",
    age: 15
}

const changeName = function (person, newName) {
    let newUser = person;
    newUser.name = newName;
    return newUser;
  };
  
  let person2 = changeName(person, "holland");
  let person3 = changeName(person2, "hardy");

  console.log(person.name); //hardy
  console.log(person2.name); //hardy
  console.log(person3.name); //hardy
📌 문제점!
같은 객체를 가리키는 새로운 변수만을 생성하고 그 변수가 가리키는 name 프로퍼티의 값을 변경해주었다. 
person~person3까지 모든 변수는 같은 주소를 바라보고 있고 그 주소공간 중 name 프로퍼티가 가리키는
데이터를 변경해서 모든 변수에 담긴 name 프로퍼티 값이 다같이 변경되는 현상이 발생한다. 

📌 불변 객체가 필요한 이유
이렇게 객체를 생성하면 원하지 않는 객체의 값마저 변경된다. 따라서 불변 객체가 필요하다. 

해결 방법

원본 객체 내부의 프로퍼티들을 복사한 새로운 객체를 생성하여 변수에 할당해줘야 한다.

변수를 선언하고, 매개변수로 전달된 person 변수를 할당하는 것이 아닌, 새로운 객체를 생성해서 person의 내부 프로퍼티들을 담아준다.

let person = {
    name : "tom",
    gender : "male",
    age: 15
}

const changeName = function (person, newName) {
		return {
				name : newName,
				gender : person.gender,
				age : person.age
		}
  };
  
  person2 = changeName(person, "holland");
  person3 = changeName(person2, "hardy");

  console.log(person.name); //tom
  console.log(person2.name); //holland
  console.log(person3.name); //hardy

3. 얕은 복사

  • 참조 : 같은 객체를 바라보고 있는 것
  • 복사를 하고 참조도 한다.
  • 객체를 직접 대입하는 경우 참조에 의한 할당이 이루어지므로 둘은 같은 데이터(주소)를 가지고 있다.
const copyObj = function (target) {
  const newObject = {}; // newObject 빈 객체 생성 

  // for in 반복문으로 새 객체에 원래 객체의 프로퍼티들을 복사하는 함수
  // target의 각 프로퍼티값을 가져올 수 있다.   
  for( let prop in target) {
      newObject[prop] = target[prop];
    }
    return newObject;
  };

let person = {
    name : "tom",
    gender : "male",
    age: 15
}

let person2 = copyObj(people);
person2.name = "holland";

console.log(person);
console.log(person2);

👉 chageName()과 차이점?

  • chageName() 함수는 이름을 변경하여 새로운 객체를 반환하는 것에 초점이 맞춰져 있어서 age, gender를 변경하기 위해서는 이를 위한 함수를 또 생성해줘야 하는데, copyObj()함수는 객체를 복사하는 과정에 초점을 맞춘 함수이다.

문제점

👉 얕은 복사 만으로는 중첩된 객체를 제대로 복사할 수 없음

copyObject() 함수에서는 새로운 객체를 생성하여 새로운 객체에 target객체의 프로퍼티들을 할당하고 있다.

하지만 target 객체 내부의 프로퍼티가 또다른 객체를 가리키는 참조변수라면 user와 user2에 담긴 friends 프로퍼티에는 같은 객체를 가리키는 주소값이 담기게 된다.

얕은 복사는 한 객체가 가지는 바로 하위의 프로퍼티들만을 복사하는 것이다. 이런 이유로 user객체가 가지고 있는 또 다른 객체인 friends 배열에 대해 복사가 이루어지지 않아서 user와 user2가 같은 friends 배열을 바라보게 된다.

let user = {
	name : "tommy",
	friends : [
		"a", "b", "c"
	]
}

user2 = copyObject(user.friends);
user2.friends[0] = "D";

console.log(user.friends); // ["D", "B", "C"]
console.log(user2.friends); // ["D", "B", "C"]

4. 깊은 복사

얕은 복사를 통해 한단계만 복사를 해서 내부에 또 객체가 있는 경우에 발생하는 문제를 해결하기 위함, 재귀 함수를 사용하는 것이 가장 좋음

재귀 함수

  • 깊은 복사는 재귀적 복사를 통해 내부에 존재하는 모든 단계의 객체들을 복사하는 것
  • 내부의 모든 값들을 찾아서 전부 복사하는 방법
  • 재귀 호출을 통해, 내부의 프로퍼티가 객체인 경우에 다시 한번 복사를 하게 되는 함수이다.
const deepCopyObject = function (target) {
    let newObject = {}; // newObject 빈 객체 생성 

    if (typeof target === "object" && target !== null) {
        for (let prop in target) {
            console.log(newObject[prop] = deepCopyObject(target[prop]));
        }
    } else {
        newObject = target;
    }
    return newObject;
  };

  let user = {
	name : "tommy",
	friends : [
		"a", "b", "c"
	]
}

user2 = deepCopyObject(user);
user2.friends[0] = "d"

console.log(user.friends);
console.log(user2.friends);

Object.assign()

  • Object.assign(생성할 객체, 복사할 객체)
  • 첫 번째 인수 : 빈 객체 생성
  • 두번 째 인수 : 할당할 객체 넣어줌
const obj = {a:1};
const newObj = Object.assign({}, obj);

newObj.a = 2;

console.log(obj) // { a: 1 }
console.log(newObj) // { a: 2 }
console.log(obj === newObj) // false 
  • 단점 : 2차원 객체의 경우 깊은 복사가 이뤄지지 않는다.
  • 중복 객체의 경우 얕은 복사가 된다.
const obj = {
    a: 1,
    b: {
      c: 2,
    },
  };
  
  const newObj = Object.assign({}, obj);
  
  newObj.b.c = 3;
  
  console.log(obj); // { a: 1, b: { c: 3 } }
  console.log(newObj)  // { a: 1, b: { c: 3 } }
  console.log(obj.b.c === newObj.b.c); // true

전개 연산자

  • 중복 객체의 경우 얕은 복사가 된다.
const obj = {
    a: 1,
    b: {
      c: 2,
    },
  };
  
  const newObj = { ...obj };
  
  newObj.b.c = 3;
  
  console.log(obj); // { a: 1, b: { c: 3 } }
  console.log(obj.b.c === newObj.b.c); // true

JSON 객체 메서드

  • 깊은 복사를 위해 JSON 객체의 stringify(), parse() 메서드를 활용할 수 있다.
  • JSON.stringify 메서드는 객체를 문자열로 치환
  • JSON.parse() 메서드는 문자열을 객체로 치환
const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const newObj = JSON.parse(JSON.stringify(obj));

newObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(obj.b.c === newObj.b.c); // false

단점

  • 다른 방법들에 비해 성능이 좋지 않다.
  • JSON.stringify 메서드는 함수를 만났을 때 undefined로 처리한다는 문제점이 있다.
const obj = {
  a: 1,
  b: {
    c: 2,
  },
  func: function() {
      return this.a;
  }
};

const newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj.func); // undefined

참고 자료

https://galid1.tistory.com/663

https://hanamon.kr/javascript-shallow-copy-deep-copy/

https://helloinyong.tistory.com/267

https://velog.io/@jujusnake/JavaScript-불변-객체-immutable-objec

https://overcome-the-limits.tistory.com/271#recentComments

profile
개인 공부 정리

0개의 댓글