Javascript - 객체 / 불변성

Cho2andy·2022년 5월 20일
0

Javascript

목록 보기
2/4
post-thumbnail

데이터의 두 가지 타입🤞

  • 원시 자료형(primitive type)과 참조 자료형(reference type)이 있다.
  • 원시 자료형이 할당될 때에는 변수에 값(value) 자체가 담긴다.
  • 참조 자료형이 할당될 때는 보관함의 주소(reference)가 담긴다.

👩‍🎓기본형 데이터 (Primitive data type) = 원시타입

  • 객체가 아니면서 method를 가지지 않는 6가지의 타입이 있다.
  • 원시 자료형은 모두 “하나”의 정보, 즉, 데이터를 담고 있다.
  • string, number, bigint, boolean, undefined, symbol, (null은 원시 타입과 거의 같게 사용되지만 엄밀히 따지면 객체이다. 빈 참조를 나타내는 데 자주 사용된다.)
  • number : 3.141592
  • string : ‘zerovodka’
  • boolean : true & false
  • undefined : 변수가 정의되지 않았거나 값이 없다.
  • null : 의도적으로 비어있음을 표현하기 위해 null 이라는 것이 들어있다.
  • symbol

🤵참조형 데이터 (Reference data type) = 참조타입

  • 자바스크립트에선 원시 자료형이 아닌 모든 것은 참조 자료형이다.
  • 참조형은 원시형 데이터의 집합이다.
  • 배열([])과 객체({}), 함수(function(){})가 대표적이다.
  • 참조 자료형은 기존에 고정된 크기의 보관함이 아니다.
  • 참조 자료형을 변수에 할당할 때는 변수에 값이 아닌 주소를 저장한다.
  • 동적으로 크기가 변하는 데이터를 보관하기위해 변수가 아닌 다른곳에 데이터를 저장하고 변수에는 그 주소만 할당한다.
  • 배열 – Array : [0,1,2,3,4]
  • 객체 – Object {name : “zerovodka”, age : 28}
  • 정규표현식(RegExp)
    <ES6에서 추가된 것들>
  • Map
  • Set
  • WeakMap
  • WeakSet

즉, 변수에는 하나의 값 혹은 주소만 저장할 수 있다.

두 타입의 가장 대표적인 차이로는 기본형에는 바로 값을 그대로 할당한다는 것이고 참조형에는 값이 저장된 주소값을 할당(참조)한다는 것이다.( ̄︶ ̄)↗ 


불변성??🤔

아니 자바스크립트에서 데이터의 불변성이라는게 있다는데 그게 뭘까???

불변성을 이해하려면 자바스크립트에서 데이터를 저장하는 방식을 알아야 한다고 한다

그럼 알아볼까요?!☝

기본형 데이터 변수를 선언합니다🙂

let a = 'abc'

a라는 변수를 선언하고 abc라는 데이터 값을 할당했다.

그럼 자바스크립트 엔진은 메모리에 a라는 식별자를 가진 빈 공간을 만들고 주소를 할당한다.
그리고 할당한 주소에 'abc'라는 값을 저장한다

즉, a라는 식별자를 가진 메모리 공간에 'abc'라는 데이터 값이 아닌 데이터 값의 메모리 주소값을 할당한다는 것이다 ( ̄︶ ̄)↗ 

그럼 변수의 값을 변경해볼까요?😀

let a = 'abc'
a = 'bbb'

선언해 둔 a 라는 변수에 'bbb'를 할당했다.

그럼 메모리에서 'bbb'라는 데이터를 찾고 없으면, 공간을 하나 더 만들어 'bbb'를 저장한다

그리고 그 주소를 a에 저장한다.

즉, a의 값을 변경했더라도 그 값을 변경한게 아니라 데이터 주소 값이 변경된 것이다 ( ̄︶ ̄)↗ 

이처럼 한 번 만든 값을 변경할 수 없는 것이 불변성이다~!!🙂

하지만 가변성이 있는 데이터가 있다!!


위에 설명했던 두 타입 중에

  • 기본형 타입 => 불변성을 띠는 데이터
  • 참조형 타입 => 가변성을 띠는 데이터

참조형 데이터 변수를 선업합니다🙂

let obj ={
		a = 1,
  		b = 'abc'
}

메모리에 obj라는 식별자를 가진 빈 공간을 마련하고, 주소를 할당한다.

그리고 그 주소에 여러개의 프로퍼티로 이루어진 데이터 그룹을 저장하기 위해 별도의 공간을 마련하고 그 영역의 주소를 저장한다

프로퍼티의 값을 변경해볼까요?😀

let obj ={
		a = 1,
  		b = 'abc'
}

obj.a = 2

obja 프로퍼티에 2를 할당하면 obj가 바라보고 있는 주소 b3030이 변경되는게 아닌 기존의 내부값만 바뀐다 ( ̄︶ ̄)↗ 

바로 이 부분 때문에 참조형 데이터는 가변값이라고 하는 것이다.

💥참고로, 참조형 데이터 자체를 변경할 경우는 불변값이고 그 내부의 프로퍼티를 변경할 때만 가변성이 성립된다.


불변 객체를 만드는 방법

자바스크립트에서 불변 객체를 만드는 방법은 2가지가 있다 ( ̄︶ ̄)↗

  • const
  • Object.freeze()

Const

  • ES6 버전부터 letconst를 지원하게 되었다

  • const 키워드는 변수를 상수로 선언할 수 있다, 일반적으로 상수로 선언된 변수는 값을 바꾸지 못하는 것으로 알려져 있다.

그렇다면 상수로 선언한 객체는 불변 객체일까??🤔

const test = {};
test.name = "seowoo";

console.log(test);  // {"seowoo"}

ES6에서의 const할당된 값이 상수가 되는 것이 아닌
바인딩된 값이 상수
가 되는, 즉 test변수가 상수가 되기 때문에
const 키워드로 선언된 test변수에는 객체 재할당은 불가능하지만 객체의 속성은 변경 가능하다.

==> 때문에 비록 재할당은 불가능하지만 객체의 속성을 변경함으로 인해 변수에 바인딩된 객체의 내용까지 변경이 되기 때문에 불변객체라고 하기는 힘들다.

Object.freeze()

자바스크립트에서 기본적으로 제공하는 메소드인 Object.freeze() 메소드이다. 공식 문서에서는 "객체를 동결하기 위한 메소드" 라고 적혀있다.

let test = {
    name : 'Choi'
}

Object.freeze(test);

test 변수에 key value를 가진 객체를 바인딩 후 Object.freeze(test)를 사용해 바인딩된 변수를 동결 객체로 만들었다. 때문에 test 객체는 객체의 속성을 변경하는 시도는 불가능하다.

test.name = 'Kim';
console.log(test) // {name: 'Choi'}

그러나 Object.freeze()는 동결된 객체를 반환하지만 객체의 재할당은 가능하다.

test = {
    age : 28
};
console.log(test); // {age: 28}

==> 때문에 Object.freeze()도 불변 객체라고 할 수는 없을 것 같다.

그럼 결국 어떻게 불변객체를 만들어??😣

constObject.freeze()를 조합하여 만들 수 있다.
(const의 재할당불가 + Object.freeze()의 객체속성 변경불가)

const test = {
    'name' : 'jung'
};

Object.freeze(test);

먼저 const키워드로 바인딩 된 변수를 상수화 시킨 다음,

Object.freeze()로 해당 변수를 동결 객체를 만들면

객체의 재할당과 객체의 속성 둘 다 변경불가능한 불변 객체가 된다.


얕은 복사 / 깊은 복사

  • 얕은 복사는 객체의 참조값(주소 값)을 복사하고,

  • 깊은 복사는 객체의 실제 값을 복사합니다.

🔅먼저 이 설명을 듣기 전에 위의 자바스크립트의 두가지 타입을 다시 보고 오는 것을 추천드립니다🔅

<깊은 복사>

  • 기본값(원시값)을 복사할 때 그 값은 또 다른 독립적인 메모리 공간에 할당하기 때문에, 복사를 하고 값을 수정해도 기존 원시값을 저장한 변수에는 영향을 끼치지 않습니다.
  • 이처럼 실제 값을 복사하는 것을 깊은 복사라고 합니다.
  • 하지만 이것은 자료형을 깊은 복사한 것입니다.

이게 무슨 말인지 아래 코드들을 보면서 이해해나가시면 됩니다.

const a = 'a';
let b = 'b';
b = 'c';

console.log(a); // 'a';
console.log(b); // 'c';

// 기존 값에 영향을 끼치지 않는다.

<얕은 복사>

  • 참조값을 복사할 때는 변수가 객체의 참조를 가리키고 있기 때문에 복사된 변수 또한 객체가 저장된 메모리 공간의 참조를 가리키고 있습니다.
  • 그래서 복사를 하고 객체를 수정하면 두 변수는 똑같은 참조를 가리키고 있기 때문에 기존 객체를 저장한 변수에 영향을 끼칩니다.
  • 이처럼 객체의 참조값(주소값)을 복사하는 것을 얕은 복사라고 합니다.
const a = {
	one: 1,
	two: 2,
};

let b = a;

b.one = 3;

console.log(a); // { one: 3, two: 2 } 출력
console.log(b); // { one: 3, two: 2 } 출력

// 기존 값에 영향을 끼친다.

깊은 복사가 뭔지는 알겠는데 얕은 복사를 해버리면 원본 객체가 수정이 되니 당황스러울 수 있을 것 같은데, 보통 복사를 하는 이유는 원본을 수정하는 것 보다는 사본을 떠서 작업을 하고 싶어 한다고 생각한다.

그럼 각각의 복사들을 하는 방법을 알고 있으면 좋을 것 같았다.

각각의 복사를 할 수 있는 방법들에 대해 알아보자( ̄︶ ̄)↗ 


<얕은 복사(Shallow Copy) 방법>

  • 얕은 복사란 객체를 복사할 때 기존 값과 복사된 값이 같은 참조를 가리키고 있는 것을 말합니다.
  • 객체 안에 객체가 있을 경우 한 개의 객체라도 기존 변수의 객체를 참조하고 있다면 이를 얕은 복사라고 합니다.

Array.prototype.slice()

  • 얕은 복사 방법의 대표적인 예입니다.😃
  • start부터 end 인덱스까지 기존 배열에서 추출하여 새로운 배열을 리턴하는 메소드 입니다.
  • 만약 start와 end를 설정하지 않는다면, 기존 배열을 전체 얕은 복사합니다.
const original = ['a',2,true,4,"hi"];
const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

copy.push(10);

console.log(JSON.stringify(original) === JSON.stringify(copy)); // false

console.log(original); // [ 'a', 2, true, 4, 'hi' ]
console.log(copy); // [ 'a', 2, true, 4, 'hi', 10 ]

==> 기존 배열에는 영향을 끼치지 않아서 깊은 복사로 보일 수 있지만, 원시값을 저장한 1차원 배열일 뿐입니다.

  • 원시값은 기본적으로 깊은 복사입니다.
  • Slice() 메소드는 기본적으로 얕은 복사를 수행합니다.
const original = [
	[1, 1, 1, 1],
	[0, 0, 0, 0],
	[2, 2, 2, 2],
	[3, 3, 3, 3],
];

const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

// 복사된 배열에만 변경과 추가.
copy[0][0] = 99;
copy[2].push(98);

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

console.log(original);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력

console.log(copy);
// [ [ 99, 1, 1, 1 ], [ 0, 0, 0, 0 ], [ 2, 2, 2, 2, 98 ], [ 3, 3, 3, 3 ] ]출력

만약 1차원 배열이 아닌 중첩 구조를 갖는 2차원 배열이면 얕은 복사를 수행하게 됩니다.

const original = [
	{
		a: 1,
		b: 2,
	},
	true,
];

const copy = original.slice();

console.log(JSON.stringify(original) === JSON.stringify(copy)); // true

// 복사된 배열에만 변경.
copy[0].a = 99;
copy[1] = false;

console.log(JSON.stringify(original) === JSON.stringify(copy)); // false

console.log(original);
// [ { a: 99, b: 2 }, true ]

console.log(copy);
// [ { a: 99, b: 2 }, false ]

배열 안에 객체를 수정하고자 할 경우 얕은 복사를 수행하는 것을 볼 수 있습니다.
하지만 원시값은 기본적으로 깊은 복사라 기존 변수에 있는 값과는 다른 값을 도출하는 것을 볼 수 있습니다.

Object.assign()

Object.assign(생성할 객체, 복사할 객체)

메소드의 첫 번째 인자로 빈 객체를 넣어주고 두 번째 인자로 복사할 객체를 넣어주면 됩니다. ( ̄︶ ̄)↗ 

const object = {
	a: "a",
	number: {
		one: 1,
		two: 2,
	},
};

const copy = Object.assign({}, object);

copy.number.one = 3;

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true

복사된 객체 copy 자체는 기존 object와 다른 객체지만 그 안에 들어가 있는 값은 기존 object안의 값과 같은 참조 값을 가리키고 있습니다.

Spread 연산자 (전개 연산자)

const object = {
	a: "a",
	number: {
		one: 1,
		two: 2,
	},
};

const copy = {...object}

copy.number.one = 3;

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // true

<깊은 복사(Deep Copy) 방법>

깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말합니다.

JSON.parse && JSON.stringify

  • JSON.stringify()는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어집니다.

  • 객체를 json 문자열로 변환 후, JSON.parse()를 이용해 다시 원래 객체(자바스크립트 객체)로 만들어줍니다.

  • 이 방법이 가장 간단하고 쉽지만 다른 방법에 비해 느리다는 것과 객체가 function일 경우, undefined로 처리한다는 것이 단점입니다.

const object = {
	a: "a",
	number: {
		one: 1,
		two: 2,
	},
	arr: [1, 2, [3, 4]],
};

const copy = JSON.parse(JSON.stringify(object));

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

재귀 함수를 구현한 복사

  • 복잡하다는 것이 단점입니다.
const object = {
	a: "a",
	number: {
		one: 1,
		two: 2,
	},
	arr: [1, 2, [3, 4]],
};

function deepCopy(object) {
	if (object === null || typeof object !== "object") {
		return object;
	}
  
// 객체인지 배열인지 판단
	const copy = Array.isArray(object) ? [] : {};
	for (let key of Object.keys(object)) {
		copy[key] = deepCopy(object[key]);
	}
	return copy;
}

const copy = deepCopy(object);

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

Lodash 라이브러리 사용

라이브러리를 사용하면 더 쉽고 안전하게 깊은 복사를 할 수 있습니다.
설치를 해야 한다는 점과 일반적인 개발에는 효율적이겠지만, 코딩 테스트에는 사용할 수 없다는 것이 단점입니다.

const deepCopy = require("lodash.clonedeep")
const object = {
	a: "a",
	number: {
		one: 1,
		two: 2,
	},
	arr: [1, 2, [3, 4]],
};

const copy = deepCopy(object);

copy.number.one = 3;
copy.arr[2].push(5);

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

console.log(object); // { a: 'a', number: { one: 1, two: 2 }, arr: [ 1, 2, [ 3, 4 ] ] }
console.log(copy); // { a: 'a', number: { one: 3, two: 2 }, arr: [ 1, 2, [ 3, 4, 5 ] ] }

공부하면서 느낀 점

단순히 코딩 공부를 위해 Javascript 문법 공부만 했지, 이렇게 이론적으로 공부를 해보니 코딩 해석 부분에서 도움이 많이 될 것 같다.

꾸준히 공부해나가야겠다.😁

profile
먹고 배우는 것엔 아끼지 말자구 ( ̄︶ ̄)↗

0개의 댓글