[Javascript 알짜개념] 불변성과 가변성, Symbol, BigInt
Symbol
- 변경이 불가한 데이터로, 유일한 식별자를 만들어 데이터를 보호하는 용도로 사용할 수 있다.
- 기본구조 : Symbol('설명') 설명 부분은 단순 디버깅을 위한 용도일 뿐, 심볼 값과는 관계가 없다.
const sKey = Symbol('Hello!')
const user = {
key: '일반 정보!',
[sKey] : '민감한 정보!'
}
console.log(user.key); //일반 정보!
console.log(user['key']); //일반 정보!
console.log(user[sKey]); //민감한 정보!
console.log(user[Symbol('Hello!')]); //undefined
console.log(sKey); //Symbol(Hello!)
const myInfo = {
firstName : 'Lee',
lastName : 'Furaha',
[birthKey] : new Date(1995,08,22)
}
for (const key in heropy){
console.log(heropy[key]);
}
//birthKey 는 출력되지 않음
Symbol 데이터는 조회할 수 없는 자료형이다.
조회하려면,
1) import 로 직접 데이터를 가지고 있고,
2) 특정 객체의 속성으로 불러줘야 조회할 수 있다.
BigInt
- BigInt는 길이 제한이 없는 정수이다.
- 숫자(number) 데이터가 안정적으로 표시하게 하는 역할을 한다.
- 최대치 (`2^53 - 1`) 보다 큰 정수를 표현할 수 있다.
- 정수 뒤에 n을 붙이거나 BigInt()를 호출해 생성할 수 있다.
console.log(123454535432325352343234232);
console.log(123454535432325352343234232n);
console.log(BigInt('123454535432325352343234232'));
const a = 11n
const b = 22
// 숫자 => Bigint
console.log(a + BigInt(b)); //33n
console.log(typeof (a + BigInt(b))); //bigint
// Bigint => 숫자
console.log(Number(a) + b); //33
console.log(typeof (Number(a) + b)); //number
Symbol이나 BigInt 두 데이터타입은 프엔 영역에서 크게 쓸 일은 없을 것 같다,,,
불변성과 가변성
- 불변성은 생성된 데이터가 메모리에서 변경되지 않는다.
- 가변성은 생성된 데이터가 메모리에서 변경될 수 있다.
- 원시형 : 불변성
- 참조형 : 가변성
불변성과 가변성을 이해하려면 메모리가 할당되는 원리를 이해해야 한다.
이 예제로 메모리 할당하는 방식을 이해해보자
let a = {x:1}
let b = a
b.x = 2
console.log(b); //{x: 2}
console.log(a); //{x: 2}
M1에 x의 값이 M3을 바라보면서
a라는 원본 객체에도 영향을 미쳤다.
왜? 객체 데이터(참조형)는 값이 지속적으로 바뀌는 가변성이기 때문이다.
그러나 원시형은 새로운 메모리 공간에 할당이 될 뿐, 메모리 할당 이후 그 공간에서는 절대 바뀌지 않는다❗️❗️
여기서 일치연산자를 더욱 이해해 볼 수 있었다
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
const arr3 = arr1;
console.log(arr1 === arr2); // 출력 결과: false (값은 같지만 다른 메모리 주소를 가리키므로 false)
console.log(arr1 === arr3); // 출력 결과: true (arr3은 arr1과 같은 메모리 주소를 가리키므로 true)
arr1과 arr2는 값은 같을지라도 각자 할당된 메모리 주소가 다르기 때문에 일치한다고 할 수 없다는 것.
메모리 할당이 어떻게 이뤄지는지 원리를 가시적으로 보는 것 같아서 매우 흥미로운 주제였다.
얕은 복사와 깊은 복사
- 참조형은 가변성으로 인해, 데이터를 복사할 때 주의가 필요하다.
- 얕은 복사(Shallow Copy) - 참조형의 1차원 데이터만 복사
- 깊은 복사(Deep Copy) - 참조형의 모든 차원 데이터를 복사
b에 a 객체를 복사해서 넣어보자.
const a = {x:1}
const b = Object.assign({},a)
b.x = 2
console.log(b); //{x:2}
console.log(a); //{x:1}
b는 a의 껍데기를 복사해서 값을 재할당하고 있다.
이렇게 껍데기를 복사해 오는 것이 얕은 복사이다.
이번엔 연산자 방법으로 얕은 복사를 해보자
const a = {x:1}
const b = {...a}
// ... spread 연산자는 객체 데이터의 중괄호를 제거한다고 생각하면 쉬움
// {...{x:1}} 여기서 중괄호 삭제해서 {x:1} 이렇게 된 것과 같음
b.x = 2
console.log(b); //{x:2}
console.log(a); //{x:1}
연산자를 사용해서 복사해오는 것도 껍데기만 복사해오고 있다.
다음은, 객체 안에 객체 차원으로 이루어진 데이터를 복사해 보자.
const a = { x: {y:1} }
const b = {...a} // 얕은 복사
b.x.y = 2
console.log(b); //x: {y:2}
console.log(a); //x: {y:2}
위 예제를 보면 제일 바깥의 껍데기만 복사했기 때문에, a의 y 값까지는 복사하지 않아서 값에 영향을 미친 것을 볼 수 있다.
결론적으로, 얕은 복사는 제일 바깥 껍데기만 복사하는 것. 이것이 1차원 데이터 복사이다.
껍데기만 복사하는 것이 아니라 참조형 안에 참조형 안에 모든 참조형 데이터를 다 복사하는 것이 깊은 복사이다.
그럼 깊은 복사하는 방법을 알아보자
npm으로 lodash 설치를 먼저 하고 거기서 cloneDeep 함수를 가져와서 쓰면 된다.
1) npm i lodash
2) package.json 에 이렇게 잘 설치되었는지 확인 후
3) npm run dev
4) import cloneDeep from 'lodash/cloneDeep' 변수 가져옴
cloneDeep 은 어디서 왔는가? (nodeModules에 cloneDeep.js 이 포함되어 있음)
import cloneDeep from 'lodash/cloneDeep'
const a = {x: {y:1} }
const b = cloneDeep(a)
b.x.y = 2
console.log(b); //x: {y: 2}
console.log(a); //x: {y: 1}
// 한 깊이 아래에 있는 데이터까지 복사해서 가져와서 원본은 건드리지 않고 있음
여기서 잠깐!! 🤚🤚🤚
그러면 filter, map, reduce 등의 배열 함수는 원본을 건드리지 않는다고 배웠는데,
그러면 이 함수들은 깊은 복사인건가❓❓❓
정답은 아니다!!! 이들은 얕은 복사이다.
처음에는 깊은 복사처럼 보일 수 있다. (새로운 메모리 주소를 가지고 있으니까).
그러나 B 내부의 객체들은 여전히 A의 원본 객체들과 같은 참조를 공유한다.
복사된 객체 B는 map() 함수를 사용하여 새로운 배열로 생성되고,
제일 바깥 껍데기는 복사되지만, 객체 내부의 중첩된 요소는 새로운 메모리를 참조하지 않고
여전히 원본 객체들을 참조하기 때문에 얕은 복사인 것이다.
대표적인 얕은 복사 중 하나도 ... spread 연산자,, 그러면 spread 연산자는 중첩되지 않은 데이터에 많이 쓰겠구나 싶었고
나중에 중첩된 객체를 가진 복잡한 데이터를 많이 다룰 것 같은데, 깊은 복사 함수를 많이 쓸 수도 있겠구나 싶었다!!
얕은 복사와 깊은 복사가 앞으로 배열과 객체를 다룰 때 중요한 개념일 것 같아서 이렇게 정리해 보았다.