- Published on
코어 자바스크립트 Chap1
- Authors
- Name
- ywj9811
데이터 타입의 종류
기본형(primitive type)
number, string, boolean, null, undefined, symbol
참조형(reference type)
- 객체(object)
- (객체 하위에 존재하는) Array, Function, Date, 정규표현식(RegExp), Map, WeakMap, Set, WeakSet
- 객체(object)
메모리와 데이터
비트 → 바이트 구성은 모두가 알 것이다.
이러한 각 비트는 고유한 식별자를 통해 위치를 확인할 수 있다.
C/C++, Java 등의 정적 타입 언어는 메모리 낭비를 최소화 하기 위해 데이터 타입별로 할당할 메모리를 나누어 정해놓음 (short는 2바이트, int는 4바이트…)
하지만, 메모리가 비교적 자유로워진 상황에서 등장한 자바스크립트는 이런 압박에서 벗어날 수 있었고, 메모리 공간을 조금 더 넉넉하게 사용할 수 있었다.
따라서 숫자의 경우 정수형/부동소수형 등등을 구분하지 않고 64비트 즉, 8바이트를 가지도록 한다. (문자열은 정해진 값이 없다)
덕분에 개발자가 형변환을 직접 해주지 않아도 된다. (Java는 int → double 등등 필요)
앞서, 각 비트는 고유한 식별자를 가진다고 했는데, 바이트 역시 시작하는 비트의 식별자로 위치를 찾을 수 있다. 즉, 모든 데이터는 바이트 단위의 식별자 → 메모리 주솟값을 통해 서로 구분하고 연결한다.
식별자와 변수
식별자와 변수를 혼용하는 경우가 많은데, 둘을 구분할 필요가 있다.
변수
말 그대로, ‘변할 수 있는 수’ 이다.
이는 ‘변할 수 있는 무언가’ 를 의미하는데, 여기서 ‘무언가’ 란 데이터를 의미한다.
식별자
식별자는 어떤 데이터를 식별하는 데 사용하는 이름, 즉 변수명을 의미한다.
변수 선언과 데이터 할당
var a;
→ “변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다” 가 위의 내용이다.
이를 보면, ‘변수’ 란 “변경 가능한 데이터가 담길 수 있는 공간 또는 그릇” 이라고 보면 정확하다.
여기까지가 변수 선언 과정이다.
var a;
a = 'abc';
// 혹은
var a = 'abc';
이를 보면, 데이터를 할당할 때 a라는 이름을 가진 주소를 검색한 후 값에 ‘abc’ 를 할당하면 될 것 같다.
하지만, 실제로는 해당 위치에 ‘abc’를 직접 할당하지 않는다.
데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서, 문자열 ‘abc’를 저장하고, 그 주소를 변수 영역에 저장하는 방식으로 이루어진다.
- 변수 영역에서 빈 공간(@1003) 확보
- 확보한 공간의 식별자를 a로 지정
- 데이터 영역의 빈 공간(@5004)에 문자열 ‘abc’ 저장
- 변수 영역에서 a라는 식별자 검색 후, 저장한 문자열의 주소를 값에 대입
이렇게 함으로써, 데이터 변환을 자유롭게 할 수 있게 됨과 동시에 메모리를 더욱 효율적으로 쓸 수 있게 해준다.
변수 영역과 데이터 영역을 구분해서 별도의 공간에 나누어 저장하게 되면, 값을 바꿀 때 복잡한 처리를 하지 않고, 새로운 데이터를 별도의 공간에 저장하고, 이후 변수가 저장하는 주소값만 변경해주면 되기 때문이다.
‘abc’의 마지막에 ‘def’를 추가한다고 하면, ‘abc’가 저장된 공간에 ‘abcdef’를 할당하는 대신, ‘abcdef’ 라는 문자열을 새로 만들어 별도의 공간에 저장하고, 그 주소를 변수 공간에 연결하는 것이다.
마찬가지로 ‘abc’에서 ‘c’를 제거하라고 해도, 새로 만들어 별도의 공간에 저장하게 된다.
이렇게 변수 영역과 데이터 영역을 분리해서, 같은 데이터를 가지는 경우 서로 다른 변수 영역이지만 같은 데이터 영역 주소를 바라보며, 변수에 저장된 값이 변경될 때는 새로운 데이터를 생성하고, 새로운 데이터 영역의 주소로 바꿔주는 방식으로 되는 것이다.
만약 이렇지 않고, 각각의 변수에 데이터를 직접 저장하는 방식을 이용한다고 가정해보자.
500개의 변수에 각각 5를 저장한다면, 숫자에 할당되는 크기의 8바이트가 500개 필요할 것이다.
그렇다면, 최소 4000바이트를 사용하게 되는 것이다.
하지만, 5를 데이터 영역에 하나 생성하고, 그 영역을 바라보게 한다면 변수 주소 영역만 500개 있으면 되는 것이기에 훨씬 효율적이라고 볼 수 있다.
기본형 데이터와 참조형 데이터
→ 불변값
변수와 상수를 구분하는 성질은 ‘변경 가능성’ 으로 바꿀 수 있으면 변수, 바꿀 수 없으면 상수이다.
이때 불변값과 상수를 같은 개념으로 오해하기 쉬운데, 명확히 구분할 필요가 있다.
→ 변수와 상수를 구분 짓는 변경 가능성의 대상은 변수 영역 메모리이다.
한번 데이터 할당이 이뤄진 변수 공간에 다른 데이터를 재할당 할 수 있는지 여부가 변수와 상수 구분의 방법이다.
→ 불변성 여부를 구분할 때 변경 가능성의 대상은 데이터 영역 메모리이다.
기본형 데이터인 숫자, 문자열, boolean, null, undefined, symbol 은 모두 불변값이다.
var a = 'abc'
a = a + 'def'
var b = 5;
var c = 5;
b = 7;
‘abc’에 ‘def’를 추가하면 ‘abc’가 ‘abcdef’로 변하는 것이 아닌, 새로운 문자열 ‘abcdef’를 만들어서 해당 주소를 변수 a에 넣어주는 방식이다.
이때 ‘abc’와 ‘abcdef’ 는 완전 별개의 데이터이다.
마찬가지로, 변수 b에 5를 할당할 때 컴퓨터는 일단 데이터 영역에서 5를 찾아보고, 없으면 새로운 데이터 공간에 5를 저장하고, 해당 주소를 넣어준다.
그리고, 변수 c에 5를 저장할 때는 b에 넣어줄 때 생성한 데이터인 5의 주소를 이용해서 새롭게 데이터 영역에 생성하지 않고 기존의 주소를 c에 넣어준다.
이때, b의 값을 7로 바꾸려고 한다면 데이터 영역에 저장된 5를 7로 바꾸는 것이 아닌, 5를 생성했을 때와 마찬가지로, 데이터 영역에서 7을 찾고, 없으면 새로 만들어서 해당 주소를 b에 다시 넣어주는 것이다.
즉, 문자열 값도 한번 만들면 바꿀 수 없고, 숫자도 한번 만들면 바꿀 수 없다.
변경은 새로 만드는 동작을 통해서만 이루어지며, 이것이 바로 불변값의 성질이다.
한번 만들어진 데이터 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.
→ 가변값
기본형 데이터는 모두 불변값이라고 했으니, 참조형 데이터는 모두 가변값이지 않을까 싶다.
기본적으로는 가변값인 경우가 많지만, 설정에 따라 혹은 활용하는 방식을 통해 불변값으로 사용할 수 있기도 하다.
var obj1 = {
a: 1,
b: 'bbb'
};
참조형 데이터를 변수에 할당하는 과정을 확인해보면 아래와 같다.
- 변수 영역의 빈 공간(@1002)를 확보하고, 그 주소의 이름을 obj1로 지정한다.
- 임의의 데이터 저장 공간(@5001)에 데이터를 저장하려고 보니, 여러개의 프로퍼티로 이루어진 데이터 그룹이다. → 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소(@7103 ~ ?) 에 @5001을 저장한다. → 객체의 프로퍼티들을 저장하기 위한 메모리 영역은 크기가 정해져있지 않고 필요한 시점에 동적으로 확보한다.
- @7103 및 @7104에 각각 a와 b라는 프로퍼티 이름을 지정한다.
- 데이터 영역에 1과 ‘bbb’ 를 저장하고, 해당 주소를 @5001 의 데이터가 바라보면 주소의 데이터에 저장한다.
여기서 눈에 띄는 차이점은 ‘객체의 변수(프로퍼티) 영역’이 별도로 존재한다는 점이다.
하지만 객체가 별도로 할애한 영역은 변수 영역일 뿐 ‘데이터 영역’은 기존의 메모리 공간을 그대로 활용하고 있다.
‘데이터 영역’ 은 여전히 불변값이다. 그러나 변수에는 다른 값들을 얼마든지 대입할 수 있다.
이 부분이 바로 참조형 데이터는 가변값이라는 이유이다.
var obj1 = {
a: 1,
b: 'bbb'
};
obj1.a = 2;
obj1 변수가 바라보는 데이터 영역은 @5001 로 그대로다.
하지만, @5001이 바라보는 @7103 ~ ? 을 보면, @7103 변수가 바라보는 데이터의 주소가 @5005 로 바뀐 것을 볼 수 있다.
즉, ‘새로운 객체’ 가 만들어진 것이 아닌, 기존의 객체 내부의 변수가 다른 데이터 영역을 바라보게 되어 객체 내부의 값이 변하게 된 것이다.
이번에는 또 다른 케이스인 참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 경우를 보자. 이를 중첩 객체라고 한다.
var obj = {
x: 3,
arr: [3, 4, 5]
};
- 컴퓨터는 우선 변수 영역의 빈 공간(@1002)를 확보하고, 그 주소의 이름을 obj로 지정
- 임의의 데이터 저장공간 (@5001)에 데이터를 저장하려는데, 이 데이터는 여러개의 변수와 값들을 모아놓은 그룹(객체)이기에, 각 변수들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소를 @5001 에 저장
- @7103에 이름 x를, @7104에 이름 arr를 지정
- 그리고, @7103에 넣을 값을 데이터 영역에 생성하고 해당 주소를 저장시킴
- @7104에 저장할 값은 배열로서 마찬가지로 데이터 그룹(객체)이다. 따라서 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 해당 주소를 저장함.
- 배열의 요소가 총 3개이므로, 3개의 변수 공간을 확보하고 각각의 인덱스를 부여
- 다시, 배열의 변수 공간에 할당할 데이터를 데이터 공간에 생성 혹은 찾아서 주소를 저장
만약 이때 다음과 같은 명령을 내리면 어떻게 될 것인가.
obj.arr = 'str'
데이터 영역에 새로운 ‘str’ 데이터를 만들고, 해당 주소 값을 @7104에 저장한다.
그러면, @5003 은 더이상 아무도 바라보지 않게 된다.
어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수를 참조 카운트라고 하는데, 참조 카운트가 0이 도면 가비지 컬렉터의 수집 대상이 된다.
따라서 @5003 은 가비지 컬렉터에게 수집 당하게 되며, @8104~8106 까지 @5003이 수집되면 연쇄적으로 가비지 컬렉터에게 수집 당하게 될 것이다.
변수 복사 비교
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'}
var obj2 = obj1
이 경우에는, 변수 영역의 a가 바라보는 데이터 영역의 주소를 b가 같이 바라보고
변수 영역의 obj1이 바라보는 데이터 영역의 주소를 obj2가 같이 바라보게 된다.
이렇게 변수를 복사하는 과정은 기본형 데이터나 참조형 데이터 모두 같은 방식이다.
하지만, 데이터 할당 과정에서 차이가 발생하여 복사 이후의 동작에도 큰 차이가 발생한다.
b = 15;
obj2.c = 20;
이것을 행하고 나면
a !== b
obj1 === obj2
이렇게 되게 된다.
왜냐하면, b는 새로운 데이터 영역을 바라보게 되어 다른 데이터를 가지게 되었지만
obj1과 obj2 가 바라보는 데이터 영역은 객체 내부의 변수 영역을 바라보고 있고, obj2.c = 20
으로 바꿨을 때는 객체 내부의 변수 영역의 c 가 바라보는 데이터 영역이 바뀐거라, 실질적으로 obj1 과 obj2 가 바라보는 데이터 영역은 변한 것이 없어서 같은 값이 되는 것이다.
하지만, 객체 자체를 변경하게 된다면 데이터 주소 자체가 달라지게 되어서 두 객체는 다른 객체가 될 수 있다.
b = 15;
obj2 = {c: 20, d: 'ddd};
이렇게 해주면, obj2가 바라보는 객체가 아예 새로운 데이터 주소에 생성되게 되어 obj1과 obj2는 각각의 데이터 주소를 바라볼 수 있게 된다.
참조형 데이터가 ‘가변값’이라고 설명할 때 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아니라, 그 내부의 프로퍼티를 변경할 때만 성립하게 된다.
불변 객체
var user = {
name: 'Jaenam',
gender: 'male'
};
var changeName = (user, newName) => {
var newUser = user;
newUser.name = newName;
return newUser;
};
var user2 = changeName(user, 'Jung');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Jung Jung
console.log(user === user2); // true
이는 객체의 가변성으로 인해 원하지 않던 데이터가 나오는 경우이다.
하지만 객체의 경우에도 데이터 자체를 변경하고자 한다면, 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않는다.
var user = {
name: 'Jaenam',
gender: 'male'
};
var changeName = (user, newName) => {
return {
name: newName,
gender: user.gender
};
};
var user2 = changeName(user, 'Jung');
if (user !== user2) {
console.log('유저 정보가 변경되었습니다.'); // 유저 정보가 변경되었습니다.
}
console.log(user.name, user2.name); // Jaenam Jung
console.log(user === user2); // false
이렇게 changeName 함수가 새로운 객체를 반환하도록 수정한다면, 가변성으로 인한 문제를 해결할 수 있다.
하지만, 이 경우 대상 객체에 정보가 많을수록 쉽지 않을 것이다.
따라서 아래와 같이 모든 프로퍼티를 복사하는 함수를 만들수도 있다.
var copyObject = function (target) {
var result = {};
for (var prop in target) {
result[prop] = target[prop];
}
return result;
}
이제는 changeUser 대신에 copyObject를 사용할 수 있을 것이다.
물론, 이런 함수를 만드는 것 외에 다양한 라이브러리를 이용할 수 있을 것이다.
얕은 복사와 깊은 복사
얕은 복사는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법이다.
위의 copyObject 함수는 얕은 복사만 수행했다.
var user = {
name: 'Jaenam',
urls: {
port: 'http://github.com',
blog: 'http://blog.com',
face: 'http://face.com'
}
}
var user2 = copyObject(user);
... (값 변경)
이런식으로 있을 때 위의 copyObject
를 이용한다면, user와 user2는 urls은 같은 데이터를 바라보게 될 것이다.
즉, user 객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터를 만들었지만, 한단계 더 들어간 객체인 urls의 내부 프로퍼티들은 기존 데이터를 그대로 참조하기 때문이다.
물론,
user2.urls = copyObject(user.urls);
이런식으로 한번 더 진행하면 다시 새로운 객체로 만들어낼 수 있다.
이때는 깊은 복사를 위해 다음과 같이 함수를 만들 수 있다.
var copyObject = function (target) {
var result = {};
if (typeof target === 'objcet' && target !== null {
for (var prop in target) {
result[prop] = copyObject(target[prop]);
}
} else {
result = target;
}
return result;
}
- 참고로,
target !== null
의 이유는, typeof명령어가 null에 대해서도object
를 주는 버그가 있어서이다.
undefined와 null
undefined 는 사용자가 명시적으로 지정할 수 있지만, 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여하는 경우가 있다.
- 값을 대입하지 않은 변수에 접근할 때 undefined반환
배열의 경우에는 약간 특이하다. →
var arr1 = []; arr1.length = 3; console.log(arr1); // [empty x 3] var arr2 = new Array[3]; console.log(arr2); // [empty x 3]
‘비어있는 요소’에 대해서는 undefined와 다르다. ‘비어있는 요소’는 순회와 관련된 많은 배열 메소드에서 아예 제외되게 된다.
배열의 함수
1. forEach
배열의 각 요소를 순회하며 특정 동작을 수행하지만, 반환값이 없음.
const arr = [1, 2, 3]; arr.forEach((num, idx) => { console.log(`Index ${idx}: ${num}`); }); arr.forEach((element, index, array) => { // element: 현재 요소 값 // index: 현재 요소의 인덱스 // array: 원본 배열 });
- 반환값:
undefined
- 용도: 단순 출력, 외부 변수 변경, 로그 남기기 등
- 특징: 다른 배열을 만들지 않음 (주로 "부수효과(side effect)"가 필요할 때)
2. map
배열의 각 요소를 변환하여 새로운 배열을 반환.
const arr = [1, 2, 3]; const doubled = arr.map(num => num * 2); console.log(doubled); // [2, 4, 6] arr.map((element, index, array) => { // element: 현재 요소 값 // index: 현재 요소의 인덱스 // array: 원본 배열 });
- 반환값: 변환된 새 배열
- 용도: 값을 가공하여 새 배열 만들기
- 특징: 원본 배열은 변경하지 않음
3. filter
조건에 맞는 요소만 걸러서 새 배열을 반환.
const arr = [1, 2, 3, 4]; const even = arr.filter(num => num % 2 === 0); console.log(even); // [2, 4] arr.filter((element, index, array) => { // element: 현재 요소 값 // index: 현재 요소의 인덱스 // array: 원본 배열 // return 값이 true인 요소만 유지 });
- 반환값: 조건을 만족하는 요소들로 구성된 새 배열
- 용도: 특정 조건에 맞는 데이터만 추출
- 특징: 원본 배열은 변경하지 않음
4. reduce
배열을 하나의 값으로 축약.
const arr = [1, 2, 3, 4]; const sum = arr.reduce((acc, cur) => acc + cur, 0); console.log(sum); // 10 arr.reduce((acc, current, index, array) => { // acc: 누적값 (이전 return 값) // current: 현재 요소 값 // index: 현재 요소의 인덱스 // array: 원본 배열 }, initialValue);
- 인자
acc
→ 누적 값(Accumulator)cur
→ 현재 요소(Current value)- 초기값(optional) 지정 가능
- 반환값: 누적 계산 결과(숫자, 문자열, 객체, 배열 등 가능)
- 용도: 합계, 곱, 객체 변환, 중첩 해제 등
- 특징: 다목적 — 다른 세 함수(map, filter)의 동작도 reduce로 구현 가능
- 반환값:
- 객체 내부에 존재하지 않는 프로퍼티에 접근할 때
- return이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a); //(1) undefined 값을 대입하지 않은 변수에 접근
var obj = { a:1 };
console.log(obj.b); // (2) 존재하지 않는 props 에 접근
var func = function() {};
var c = func();
console.log(c); //(3) 명시적인 반환값이 없는 함수에 반환값
값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 대해서는 자바스크립트가 undefined를 할당한다. → var 는 이렇게 동작한다. 하지만, let과 const는 undefined를 할당하지 않은 채 초기화를 마치며, 이후 실제 변수가 평가되기 전까지 접근할 수 없다.
undefined와 null 모두 아무것도 할당하지 않았을 때 사용할 수 있고, 둘 다 사람이 직접 할당할 수 있다. 따라서 undefined를 직접 할당하기 보다는 직접 할당할 경우 null 을 사용하도록 하자. (혼란 방지)