정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.
🧐변수는 어떤 원리(과정)로 선언되어질까?
//📍예제 1-1 변수선언
var a;
예제 1-1을 말로 풀어쓰면,
"변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다” 가 된다.
변수란 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇
이 공간에 숫자를 담았다가 문자열을 담을 수도 있다
변수를 선언한다는 것은
메모리에서 비어있는 공간을 확보하고 그 공간의 이름을 설정하는 과정!
🧐선언한 변수에 데이터는 어떻게 할당되어 질까?
//📍예제1-2
var a; //변수 a 선언
a = 'abc'; //변수 a에 데이터 할당
var a ='abc' //변수 선언과 할당을 한 문장으로 표현
데이터 할당에 대한 메모리 영역의 변화
(데이터의 성질에 따라 "변수영역"과 "데이터 영역"으로 나누어보자)
- 변수 영역에서 빈 공간(@1003) 확보
- 확보한 공간의 식별자를 a로 지정
- 데이터 영역의 빈 공간(@5004)에 문자열 ‘abc’를 저장
- 변수 영역에서는 a라는 식별자를 검색(@1003)
- 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입
⇒ 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더욱 효율적으로 관리하기 위해서
문자열 같은 데이터타입은 그 문자가 영어인지(1바이트), 한글(2바이트)인지 또는 문자열의 길이에 따라 메모리를 가변적으로 사용해야하기 때문이다.
데이터영역과 변수영역을 별도의 공간으로 나누어 저장하는 것이 최적이다.
💡 명확하게 구분하기
기본형데이터(숫자, 문자열, boolean, null, undefined, Symbol)은 모두 불변값!
🤔기본형데이터에서
새로운 변수 b가 선언되고, 기존의 변수 a와 동일한 데이터가 할당된다면?
//📍예제1-3
var a = 10;
var b = 10;
예제 1-3의 경우에는 또 다른 변수 b를 더 선언했다. 그렇다면 b의 데이터 할당은 어떻게 될까? 여기서 중요한 것은 10이라는 값이 새로운 주소에 할당되고 b에 해당 주소가 대입되는 것이 아니라는 것이다. b는 a의 10과 같은 주소를 참조하고 새로운 데이터 영역을 더 만들지 않는다.
결국 자바스크립트 엔진은 예제 1-3을 실행할때 다음과 같은 절차를 거친다.
- 변수 영역에 빈 공간을 확보하여 식별자 a로 지정하고,
- 데이터 영역에서 10이 있는지 검색한 후 10을 찾지 못했기 때문에 새로운 주소에 10을 저장하고,
- a 식별자 주소에 10 데이터 주소를 대입한다.
- 변수 영역에 빈 공간을 확보하여 식별자 b로 지정하고,
- 데이터 영역에서 10이 있는지 검색한 후 10이 있는 것을 확인한 후,
해당 데이터 주소를 b 식별자 주소에 대입한다.
🤓불변값의 성질
문자열 값도 한 번 만든 값을 바꿀 수 없고, 숫자 값도 다른 값으로 변경할 수 없다.
변경은 새로 만드는 동작을 통해서만 이루어진다.이것을 불변값의 성질이라고 한다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.
즉, 데이터의 변화가 있을 때 해당 데이터를 수정하는 것이 아니라, 새로운 수정된 데이터 영역을 새로 만든다(추가한다).
참조형 데이터의 기본적인 성질은 가변값인 경우가 많다.
하지만, 설정에 따라 변경불가능한 경우(*Object.defineProperty, Object.freeze등)도 있고, 아예 불변값으로 활용하는 방안도 있다.
🤔그럼 Object(참조형데이터)의 경우는 어떻게 데이터 할당이 이루어질까?
//📍예제1-4
var obj1 = {
a:1,
b:'bbb'
};
- 변수 영역의 빈 공간(@1003)을 확보하고, 그 주소의 이름을 obj1으로 지정
- 임의의 데이터 저장공간(@5002)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이루어진 데이터 그룹이다. 내부 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소(@7103~?)를 @5002에 저장
- @7103과 @7104에 각각 a와 b라는 프로퍼티 이름 지정
- 데이터 영역에서 숫자 1을 검색하고 결과가 없으므로 @5003에 저장하고 이 주소를 @7103에 저장
- 문자열 ‘bbb’ 역시 임의로 5004에 저장하고, 이 주소를 @7104에 저장
📌기본형데이터와의 차이는
참조형데이터는 '객체의 변수(프로퍼티) 영역'이 별도로 존재한다
그리고 데이터 영역은 기존의 메모리공간(@5003,@5004)을 그대로 활용한다
→ 데이터영역에 저장된 값은 모두 불변값이다. 그러나 변수 영역에는 다른 값을 얼마든지 대입 가능! 그래서 흔히 참조형 데이터는 불변하지않다(가변값이다,mutable)라고 한다.
참조형 데이터의 프로퍼티 재할당
//📍예제1-5
var obj1 = {
a:1,
b:'bbb'
};
obj1.a = 2;
4번째 줄까지는 예제1-4와 동일하므로 제일 마지막줄만 보면,obj1의 a프로퍼티에 숫자 2를 할당하려고 한다.
1. 데이터영역에서 숫자 2를 검색한다
2. 검색결과에 2가 없으므로 빈 공간 @5005에 저장하고
3. 이 주소를 @7103에 저장한다.
변수 obj1이 바라보고 있는 주소는 @5001로 변하지 않았다.
즉, '새로운객체'가 만들어진 것이 아니라 기존의 객체 내부의 값만 변경 되었다.
데이터 영역에 데이터를 할당하기 전에 먼저 그 데이터가 데이터영역에 있는지 부터 검사한다.
있으면 해당 데이터의 주소를 가져다 쓰고, 없으면 새로운 데이터를 데이터 영역에 생성하여 그 데이터의 주소를 쓴다.
📌기본형과 참조형의 차이는
단순히 주소를 참조하느냐가 아니라 주소를 복사하는 과정이 한 번 더 거치는지의 차이다.
다시말해, 기본형의 값 복사는 본래 갖고 있던 데이터의 주솟값만 복제를 하기 때문에 불변성이 유지가 되는 것이고!! 참조형의 값 복사는 본래 갖고 있던 데이터의 주솟값을 동일하게 갖고 있기 때문에, 복사를 한 값에서 변화를 주면 원래 갖고 있는 값도 영향을 받기 때문이다.
불변성은 최근 React, Vue, Agnular등의 라이브러리뿐만 아니라, 디자인 패턴, 함수형 프로그래밍등에서 아주 주요한 개념으로 여긴다.
불변성은 한 객체의 내부 프로퍼티가 바뀔 때, 원본 객체는 바뀌지 않고 새로운 객체를 만들거나 만들기로 해서 확보가 가능하다. 어떻게?!
//📍객체의 가변성에 따른 문제점
let user = {
name: '소주',
gender: 'female'
}
let changeName = function(user,newName){
let newUser = user
newUser.name = newName
};
let user2 = changeName(user, '맥주')
if(user !==user2){
console.log('유저정보가 변경되었습니다!')
}; //안나옴
console.log(user.name, user2.name) //맥주 맥주
console.log(user === user2) //true
위의 코드는 앞에서 본 것과 같이 user2와 user1에 할당된 객체의 주소값이 같으므로 user2.name이 업데이트 되니까 user.name을 찍었을때 바뀐 값이 출력이 된다.
따라서,
//📍객체의 가변성에 따른 문제점 해결방법 → 변경 전과 후에 서로 다른 객체를 바라보도록 만들어보자
let user = {
name: '소주',
gender: 'female'
}
let changeName = function(user, newName){
return {
name: newName,
gender: user.gender
};
let user2 = changeName(user, '맥주')
if(user !==user2){
console.log('유저정보가 변경되었습니다!')
}; //유저정보가 변경되었습니다!
console.log(user.name, user2.name) //감자 배추
console.log(user === user2) //false
이렇게 바로 chageName함수에서 새로운 객체를 반환을 하게되면 해결을 할 수 있다. 그러나 이것도 문제가 있는것이 gender(변경할 필요가 없는 기존 객체의 프로퍼티)를 하드코딩 했기 때문이다. 만약에 이런 프러퍼티가 여러개라면? 다 하드코딩을 해야하므로 비효율적이다. 그때 for in문법을 사용하면 된다!
//📍기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은복사)
let copyObj = function(target){
let result = {}
for(let prop in target){
result[prop] = target[prop]
}
return result
}
//copyObj를 이용한 객체 복사
let user = {
name: '소주',
gender: 'female'
}
let user2 = copyObj(user)
user.name = '맥주'
if(user !==user2){
console.log('유저정보가 변경되었습니다!')
}; //유저정보가 변경되었습니다!
console.log(user.name, user2.name) //소주 맥주
console.log(user === user2) //false
이렇게 copyObj함수를 만들어서 간단하게 객체를 복사하고, 수정 할 수 있다. 그러나 immer.js, baobab.js같은 휼륭한 라이브러리가 있으니 그것을 사용하자~
얕은 복사는 바로 아래단계의 값만 복사하는 값이고, 깊은 복사는 내부의 모든 값들을 하나하나 찾아 전부 복사하는 방법니다. 즉 얕은복사는 참조데이터의 참조변수들의 메모리 주소까지만 복사를 하기때문에 복사한 데이터의 참조값을 바꿔도 원본 데이터가 바뀐다(기존 데이터를 그대로 참조하니까).
let deepCopyObj = function (target) {
let result = {};
if (typeof target === "object" && target !== null) {
//target !== null를 넣어준 이유는, typeof 명령어가 null에 대해서도 object를 반환하는 버그가 있기 때문이다.
for (let prop in target) {
result[prop] = deepCopyObj(target[prop]);
}
} else {
result = target;
}
return result;
};
let obj = {
a:1,
b:{
c:null,
d:[1,2]
}
}
let obj2 = deepCopyObj(obj)
obj2.a = 3;
obj.b.c = 4;
obj.b.d[1] = 3
console.log(obj)
/*{
a:1,
b:{
c:null,
d:[1,3]
}
}*/
console.log(obj2)
/*
{
a:3,
b:{
c:4,
d:[1,2]
}
}
*/
깊은복사는 이렇게 재귀적인 로직을 통해 내부 프로퍼티들을 순회하여 target을 그대로 지정하게끔 했다.
obj와 obj2는 아예 다른 주소값을 갖는 객체들이다.따라서 저 둘은 서로 영향을 주지 않는다.
다른 방식으로는 JSON.stringfy()로 문자열로 바꾼 다음 다시 JSON화를시켜주어도 된다. 다만 prototype나 getter/setter와 같은 경우 적용이 되질 않는다.
JS에선 없음을 의미하는 값이 두가지가 있는데 그게 바로 null과 undifiend이다. 둘의 차이는 무엇일까? 각 각 어떤 목적으로 사용될까?
undefined은 사용자가 명시적으로 지정할 수도 있으나, 값이 존재하지 않을때 JS엔진이 자동을 부여하는 경우가 있다.
JS엔진은 사용자가 어떤 값을 지정할 것이라고 예상되는 상황인데도 실제로 그렇게 안 할때 undefined를 반환한다. 아래의 세 경우가 이에 해당한다.
1) 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않는 식별자에 접근할 때
let a;
console.log(a) //undefined
2) 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
let drink = { a:'beer'}
console.log(drink.b) // undefined
3) return문이 없거나 호출되지 않는 함수의 실행결과
let func = function(){}
console.log(func()) //undefined return값이 없으면 undefined를 반환한 것으로 간주함
그러나 1)의 경우 데이터가 배열일 때 조금 독특한 동작이 있다.
let arr1 = [];
arr1.length = 3
console.log(arr1) //[empty x 3]
let arr2 = new Array(3)
console.log(arr2) //[empty x 3]
let arr3 = [undefined, undefined, undefined]
console.log(arr3) //[undefined, undefined, undefined]
첫번째, 두번째 예를 보면,3개의 빈 요소를 확보했지만 문자 그대로 어떤 값도, undefined값 조차도 할당되어 있지 않다. 이는 '비어있는 요소'와 'undefined를 할당한 요소'가 다르다는 것을 의미한다.*'비어있는 요소'는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다.
그래서 출력 결과도 당연히다르다.
let arr1 = [undefined, 1]
let arr2 = []
arr2[1] = 1;
arr1.forEach((v,i) => console.log(v,i))
//undefined 0 / 1 1
arr2.forEach((v,i) => console.log(v,i))
//1 1
arr1.map((v,i) => v+i)
//[Nan, 2]
arr2.map((v,i) => v+i)
//[empty, 2]
arr1.filter((v)=> !v)
//[undefined]
arr2.filter((v)=> !v)
//[]
arr1.reduce((p,c,i)=>p+c+i,'')
//undefined011
arr2.reduce((p,c,i)=>p+c+i,'')
//11
보다시피 arr2에서 각 메소드들이 비어있는 요소에서는 어떠한 처리도 하지 않았음을 알 수 있다.
이 이유는 배열도 객체라는 것을 생각해보면 당연한 것이다.
null은 비어있음을 명시적으로 나타내고 싶을때 사용하는 것이다. 그러나 typeof null이 object라고 나오는 버그가 있으니null여부는 다른식으로 접근해야하는데
let isNull = null
console.log(isNull === null) //true
바로 일치 연산자를 확인해보면 된다!!!
[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://hsolemio-lee.github.io/core-javascript1/
https://velog.io/@holasim/JS%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%83%80%EC%9E%85