목차
- 원시값의 메서드
- 숫자형
- 문자열
- 배열
- 배열과 메서드
- iterable 객체
- 맵과 셋
- 위크맵과 위크셋
- Object.keys, values, entries
- 구조 분해 할당
- Date 객체와 날짜
- JSON과 메서드
원시값의 메서드
원시값:
- 원시형 값이다.
- 원시형의 종류는 문자(string), 숫자(number), bigint, 불린(boolean), 심볼(symbol), null, undefined 형으로
총 일곱 가지 이다.
객체:
- 프로퍼티에 다양한 종류의 값을 저장할 수 있다.
- {name: "John", age: 30}와 같이 대괄호 {}를 사용해 만들 수 있다.
함수도 객체의 일종이다.
숫자형
숫자를 입력하는 다양한 방법
let billion = 1000000000;
billion = 1bn;
billion = 1e9; // 1과 9개의 0
숫자 옆에 'e'를 붙이고 0의 개수를 그 옆에 붙여주면 숫자를 줄일 수 있다.
즉, 10의 거듭제곱이라고 생각하면된다.
16진수, 2진수, 8진수
16진수는 0x를 사용해 표현할 수 있다.
alert(0xff); // 255
alert(0xFF); // 255 (대,소문자를 가리지 않는다)
2진수와 8진수는 0b와 0o를 사용해 간단히 나타낸다.
(아마 b=binary, o=octa...?)
let a = 0b11111111; // 255의 2진수
let b = 0o377; // 255의 8진수
alert(a == b); // true
toString(base)
num.toString(base) 메서드는 base 진법으로 num을 표현한 후, 이를 문자형으로 변환해 반환한다.
let num = 255;
alert(num.toString(16)); // ff
alert(num.toString(2)); // 11111111
base는 2에서 36까지 사용 가능하고, default는 10이다.
- base=16 - 16진수 색, 문자 인코딩 등을 표현할 때 사용한다. 숫자는 0부터 9, 10이상의 수는 A부터 F를 사용한다.
- base=2 - 비트 연산 디버깅에 주로 쓰인다. 숫자는 0또는 1이 될 수 있다.
- base=36 - 0..9와 A..Z를 사용해 숫자를 표현한다. url을 줄이는 것과 같이 숫자로 된 긴 식별자를 짧게 줄일 때
유용하다
alert(123456..toString(36)); // 2n9c
<!> 123456.toString(36)처럼 점(.)을 한개만 사용한다면 점 이후로 소수로 인식되어 에러가 발생할 수 있다.
숫자를 대상으로 toString을 사용할 경우 .을 두개찍어야 한다.
점을 하나만 쓰고 싶다면 (123456).toString(36)도 가능하다.
어림수 구하기
- Math.floor : 소수점 첫째 자리에서 내림
- Math.ceil : 소수점 첫째 자리에서 올림
- Math.round : 소수점 첫째 자리에서 반올림
- Math.trunc : 소수부를 무시
부정확한 계산
숫자는 내부적으로 64비트 형식 IEEE-754으로 표현되기 때문에 숫자를 저장하려면 정확히 64비트가 필요하다.
64비트중 52비트는 숫자를 저장하는 데 사용되고, 11비트는 소수점 위치를(정수는 0), 1비트는 부호비트이다.
그런데 숫자가 너무 커지면 64비트 공간이 넘쳐서 Infinity로 처리된다.
alert(1e500); // Infinity
꽤 자주 발생하는 정밀도 손실도 있다.
alert(0.1 + 0.2 == 0.3); // false 0.30000....04
숫자는 0과 1로 이루어진 이진수로 변환되어 연속된 메모리 공간에 저장된다.
그런데 10진법을 사용하면 쉽게 표현할 수 있는 0.1, 0.2 같은 분수는 이진법으로 표현하면 무한 소수가 된다.
10의 거듭제곱으로 나눈 값은 10진법에서 잘 동작하지만 3으로 나누게 되면 10진법에서 무한소수가 된다.
같은 이유로 2진법에서 2의 거듭제곱으로 나누지 않은 수는 무한 소수가 되어버린다.
문제를 해결할 방법은 toFixed(n) 메서드를 사용해 어림수를 만드는 것이다.
let sum = 0.1 + 0.2;
alert(sum.toFixed(2)); // 0.30
단 toFixed를 이용시 문자열형으로 변환되기 때문에 앞에 +를 붙인다던가, 수학연산 혹은 정수형 변환이 필요하다.
isNaN과 isFinite
- isNaN(value) - 인수를 숫자로 변환한 다음 NaN인지 테스트한다.
alert(isNaN(NaN)); // true
alert(isNaN("str")); // true
alert(NaN === NaN); // false
NaN은 자기 자신을 포함하여 어떤 값과도 같지 않다.
- isFinite(value) - 인수를 숫자로 변환하고 변환한 숫자가 NaN/Infinity/-Infinity가 아닌 일반 숫자인경우 true 리턴
alert(isFinite("15")); // true
alert(isFinite("str")); // false, NaN이다.
alert(isFinite(Infinity)); // false, Infinity다.
parseInt와 parseFloat
+ 또는 Number()를 사용하여 숫자형으로 변환시 피연산자가 숫자가 아니면 형 변환이 실패한다.
예를들어 +"12px"은 NaN이 된다.
하지만 parseInt와 parseFloat는 불가능할 때까지 문자열에서 숫자를 읽는다.
숫자를 읽는 도중 오류가 발생하면 이미 수집된 숫자를 반환한다.
하지만 "a123" 같은 문자열은 a를 만나 중지되기 때문에 NaN을 반환한다.
문자열
자바스크립트에는 글자 하나만 저장할 수 있는 별도의 자료형이 없다.
특수 기호
let str1 = "Hello\nWorld"; // '줄 바꿈 기호'를 사용해 두 줄짜리 문자열을 만든다.
// 백틱과 일반적인 줄 바꿈 방법(엔터)을 사용해 두 줄짜리 문자열을 만든다
let str2 = `Hello
World`;
alert(str1 == str2); // true
특수 문자
- /n : 줄바꿈
- /uXXXX : utf-16 인코딩 규칙을 사용하는 16진수 코드 XXXX로 표현한 유니코드 기호
- /u{X...XXXXX} (한 개에서 여섯 개 사이의 16진수 글자) : utf-32로 표현한 유니코드 기호
alert("\u00A9"); // Copyright 기호
alert("\u{20331}"); // 중국어(긴 유니코드)
alert("\u{1F60D}"); // 웃는 얼굴 기호(긴 유니코드)
문자열의 길이를 알고싶다면 문자열.length를 사용하면된다.
\n도 하나의 기호이기때문에 "abc\n".length는 4이다.
특정 글자에 접근하기
문자열 내 특정 위치인 pos에 있는 글자에 접근하려면 [pos]같이 대괄호를 이용하거나 str.charAt(pos)라는 메서드를
호출하면 된다. 위치는 0부터 시작한다.
let str = `Hello`;
// 첫 번째 글자
alert(str[0]); // H
alert(str.charAt(0)); // H
// 마지막 글자
alert(str[str.length - 1]); // o
// 차이점
alert(str[1000]); // undefined
alert(str.charAt(1000)); // ''(빈 문자열)
대,소문자 변경하기
alert('Interface'.toUpperCase()); // INTERFACE
alert('Interface'.toLowerCase()); // interface
부분 문자열 찾기
str.indexOf
str.indexOf(substr, pos) : str의 pos에서부터 시작해, 부분 문자열 substr이 어디에 위치하는지 찾아준다.
let str = 'Widget with id';
alert(str.indexOf('Widget')); // 0, str은 'Widget'으로 시작한다.
alert(str.indexOf('widget')); // -1, indexOf는 대,소문자를 따지므로 원하는 문자열을 찾지 못한다.
alert(str.indexOf('id')); // 1, "id"는 첫 번째 위치에서 발견된다(Widget의 id)
alert(str.indexOf('id', 2)); // 12, 두번째로 등장하는 id를 찾는다
주의점!
let str = "Widget with id";
if(str.indexOf("Widget") != -1) { // -1을 안적을시 0으로 간주되어 false 처리가된다
alert("find");
}
비트 NOT 연산자를 사용한 기법
비트 NOT 연산자는 피연산자를 32비트 정수로 바꾼 후(소수부는 모두 버려짐) 모든 비트를 반전한다.
따라서 n이 32비트 정수일 때 ~n은 -(n+1)이 된다.
~n이 0이 되는경우는 n이 -1일때 이므로 활용하면 된다.
let str = "Widget";
if(~str.indexOf("Widget")) {
alert("find");
}
오래된 기법으로, 잘 쓰이진 않지만 오래된 페이지에서 종종 볼 수 있다.
includes, startsWith, endsWith
alert("Widget with id".includes("widget")); // true
alert("Widget".includes("id", 3)); // false, 세 번째 위치 이후엔 "id"가 없다.
alert("Widget".startsWith("Wid")); // true
alert("Widget".endsWith("get")); // true
부분 문자열 추출
str.slice(start [, end])
문자열의 start부터 end까지(end는 미포함)를 반환한다.
let str = "stringify";
alert(str.slice(0, 5)); // "strin"
alert(str.slice(2)); // "ringify", 2번째부터 끝까지
alert(str.slice(-4, -1)); // "gif", 끝에서 4번째부터 시작해 끝에서 1번째 위치까지
str.substring(start [, end])
start와 end 사이에 있는 문자열을 반환한다.
slice와 유사하지만 start가 end보다 커도 된다.
음수는 허용하지 않는다, 음수는 0으로 처리된다.
let str = "stringigy";
alert(str.substring(6,2)); // "ring"
alert(str.slice(6,2)); // "" (빈 문자열)
str.substr(start [, length])
start부터 시작해 length개의 글자를 반환한다
let str = "stringify";
alert(str.substr(2, 4)); // ring, 두 번째부터 글자 네 개
alert(str.substr(-4, 2)); // gi, 끝에서 네 번째 위치부터 글자 두 개
* slice만 외워놓고 사용해도 충분할 것 같다
문자열 비교하기
- 소문자는 대문자보다 항상 크다.
- 발음 구별 기호(diacritical mark)가 붙은 문자는 알파벳 순서 기준을 따르지 않는다.
자바스크립트 내부에서 모든 문자열은 utf-16을 사용해 인코딩되는데, utf-16에선 모든 글자가 숫자 형식의 코드와
매칭된다.
str.codePointAt(pos)
pos에 위치한 글자의 코드를 반환한다.
// 글자는 같지만 케이스는 다르므로 반환되는 코드가 다르다
alert("z".codePointAt(0)); // 122
alert("z".codePointAt(0)); // 90
String.fromCodePoint(code)
숫자 형식의 code에 대응하는 글자를 만들어준다.
alert(String.fromCodePoint(90)); // Z
문자열 제대로 비교하기
문자열을 비교하려면 일단 페이지에서 어떤 언어를 사용하고 있는지 브라우저가 알아야 한다.
모던 브라우저 대부분이 국제화 관련 표준인 ECMA-402를 지원한다.
ECMA-402에는 메서드가 정의되어있다.
str.localeCompare(str2)를 호출하면 정수를 반환해준다
- str이 str2보다 작으면 음수를 반환한다.
- str이 str2보다 크면 양수를 반환한다.
- str과 str2가 같으면 0을 반환한다.
배열
배열 선언
let arr = new Array();
let arr = [];
pop,push와 shift,unshift
큐라는 배열을 사용해 만들 수 있는 대표적인 자료구조로, 배열과 마찬가지로 순서가 있는 컬렉션을 저장하는 데
사용한다.
- push - 맨 끝에 요소를 추가한다.
- shift - 제일 앞 요소를 꺼내 제거한 후 남아있는 요소들을 앞으로 밀어준다.(두번째 요소가 첫번째 요소가된다)
배열엔 두 연산을 가능하게 해주는 내장 메서드 push와 pop이있다.
배열은 큐 이외에 스택이라 불리는 자료구조를 구현할 때도 쓰인다.
- push - 요소를 스택 끝에 집어넣는다.
- pop - 스택 끝 요소를 추출한다.
let fruits = ["사과", "오렌지", "배"];
/* 배열 끝에 무언가를 해주는 메서드 */
fruits.pop(); // "배" 제거
alert(fruits); // 사과, 오렌지
fruits.push("배"); //
alert(fruits); // 사과, 오렌지, 배
/* 배열 앞에 무언가를 해주는 메서드 */
fruits.shift(); // "사과" 제거
alert(fruits); // 오렌지, 배
fruits.unshift("사과");
alert(fruits); // 사과, 오렌지, 배
배열의 내부 동작 원리
배열은 특별한 종류의 객체이다. arr[0]처럼 접근하는것도 객체 문법에서 왔다.
다만 배열은 키가 숫자라는 점이다.
배열은 원시 자료형에 해당하지 않고, 객체형에 속하기 때문에 객체처럼 동작한다.
배열은 객체와 마찬가지로 참조를 통해 복사된다.
let fruits = ["바나나"];
let arr = fruits; // 참조 복사(두 변수가 같은 객체를 참조)
alert(arr === fruits); // true
arr.push("배"); // 참조를 이용해 배열을 수정
alert(fruits); // 바나나, 배 - 요소가 두개가 되었다.
성능
push와 pop은 빠르지만 shift와 unshift는 느리다.
배열 앞에 무언가를 해주는 메서드가 배열 끝에 무언가를 해주는 메서드보다 느린 이유는 무엇일까?
- 인덱스가 0인 요소를 제거한다.
- 모든 요소를 왼쪽으로 이동시킨다. 이때 인덱스 1은 0, 2는 1로 변한다.
- length 프로퍼티 값을 갱신한다.
따라서 배열에 요소가 많으면 많을수록 이동횟수가 많아져 느리다.
하지만 pop과 push는 인덱스는 유지하되, 끝값만 제거 혹은 추가를 해주고 length 프로퍼티에 변화만 주면된다.
반복문
배열을 순회할 때 for문을 사용하지만, for..of를 사용하는 방법도 있다.
let fruits = ["사과", "오렌지", "자두"];
// 배열 요소를 대상으로 반복 작업을 수행한다.
for(let fruit of fruits) {
alert(fruit);
}
그리고 배열은 객체형에 속하므로 for..in도 사용가능하다.
하지만 배열엔 되도록 for..in은 사용하지 않는것이 좋다.
(객체와 사용할때 최적화 되있어서 배열과 사용하면 병목현상 발생가능)
'length' 프로퍼티
length를 줄였을때 삭제된 값은 다시 늘려도 복구되지 않는다.
length 프로퍼티를 0으로 설정하면 간단하게 배열을 비울 수 있다.
배열과 메서드
요소 추가,제거 메서드
splice
배열에서 요소를 지우려면 배열 역시 객체형이므로 프로퍼티를 지울 때 쓰는 연산자 delete를 사용할 수 있다.
하지만 key에 해당하는 값을 지우기 때문에 length는 그대로이다.
arr.splice(start)는 요소 추가, 삭제, 교체가 모두 가능하다.
삭제
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // 인덱스 1부터 요소 한 개를 제거
alert(arr); // ["I", "JavaScript"]
교체
let arr = ["I", "study", "JavaScript", "right", "now"];
// 처음(0) 세 개(3)의 요소를 지우고, 이 자리를 다른 요소로 대체한다.
arr.splice(0, 3, "Let's", "dance");
alert(arr) // now ["Let's", "dance", "right", "now"]
추가
let arr = ["I", "study", "JavaScript"];
// 인덱스 2부터
// 0개의 요소를 삭제.
// 그 후, "complex"와 "language"를 추가.
arr.splice(2, 0, "complex", "language");
alert(arr); // "I", "study", "complex", "language", "JavaScript"
slice
arr.slice([start], [end])
이 메서드는 "start" 인덱스부터("end"를 제외한) "end" 인덱스까지의 요소를 복사한 새로운 배열을 반환한다.
arr.slice는 문자열 메서드인 str.slice와 유사하게 동작하는데 arr.slice는 서브 문자열(substring) 대신
서브 배열(subarray)을 반환한다.
let arr = ["t", "e", "s", "t"];
alert(arr.slice(1, 3)); // e, s
alert(arr.slice(-2)); // s, t (인덱스가 -2인 요소부터 제일 끝 요소까지를 복사)
<!> arr.slice()는 인수를 하나도 넘기지 않고 호출하여 arr의 복사본을 만들 수 있다.
이런 방식은 기존의 배열을 건드리지 않으면서 배열을 조작해 새로운 배열을 만들고자 할 때 자주 사용된다.
concat
arr.concat은 기존 배열의 요소를 사용해 새로운 배열을 만들거나 기존 배열에 요소를 추가하고자 할 때 사용할 수 있다.
문법 : arr.concat(arg1, arg2...)
let arr = [1, 2];
// arr의 요소 모두와 [3,4]의 요소 모두를 한데 모은 새로운 배열이 만들어진다.
alert(arr.concat([3,4])); // 1,2,3,4
concat 메서드는 제공받은 배열의 요소를 복사해 활용한다. 객체가 인자로 넘어오면 객체는 분해되지 않고 통으로
복사되어 더해진다.
그런데 인자로 받은 유사 배열 객체에 특수한 프로퍼티 Symbol.isConcatSpreadable이 있으면 concat은 이 객체를
배열처럼 취급한다. 따라서 객체 전체가 아닌 객체 프로퍼티의 값이 더해진다.
let arr = [1, 2];
let arrayLike1 = {
0: "something",
1: "else",
length: 2
};
let arrayLike2 = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert(arr.concat(arrayLike1)); // 1,2,[object Object]
alert(arr.concat(arrayLike2)); // 1,2,something,else
forEach로 반복작업하기
arr.forEach는 주어진 함수를 배열 요소 각각에 대해 실행할 수 있게 해준다.
// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
배열 탐색하기
indexOf, lastIndexOf와 includes
이름이 같은 문자열 메소드와 하는일이 같다
- arr.indexOf(item, from)은 인덱스 from부터 시작해 item(요소)을 찾는다.
요소를 발견하면 해당 요소의 인덱스를 반환하고, 발견하지 못했으면 -1을 반환한다. - arr.lastIndexOf(item, from)는 위 메서드와 동일한 기능을 하는데, 검색을 끝에서부터 시작한다
- arr.includes(item, from)는 인덱스 from부터 시작해 item이 있는지를 검색하는데, 해당하는 요소를 발견하면
true를 반환한다.
find와 findIndex
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let user = users.find(item => item.id == 1);
alert(user.name); // John
filter
find 메서드는 함수의 반환 값을 true로 만드는 단 하나의 요소를 찾는다.
조건을 충족하는 요소가 여러 개라면 arr.filter(fn)를 사용하면 된다.
filter는 find와 문법이 유사하지만, 조건에 맞는 요소 전체를 담은 배열을 반환한다는 점에서 차이가 있다.
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// 앞쪽 사용자 두 명을 반환한다.
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
배열을 변형하는 메서드
map
map은 배열 요소 전체를 대상으로 함수를 호출하고, 함수 호출 결과를 배열로 반환해준다.
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5, 7, 6
sort
arr.sort()는 배열의 요소를 정렬해준다. 배열 자체가 변경된다.
하지만 요소는 문자열로 취급되어 재 정렬되기 때문에 숫자로 정렬시키려면 아래와 같이 해야한다.
function compareNumeric(a, b) {
if(a > b) return 1;
if(a == b) return 0;
if(a < b) return -1;
}
let arr = [1, 2, 15];
arr.sort(compareNumeric);
alert(arr); // 1, 2, 15
<!> 정렬함수는 어떤 값이든 반환할 수 있다.
<!> 양수를 반환하는경우 첫째값이 크다, 음수를 반환하는 경우 둘째값이 크다를 나타내기만 하면된다.
arr.sort((a, b) => a - b);
<!> 문자열엔 localeCompare를 사용하는것이 좋다.
reverse
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert(arr); // 5, 4, 3, 2, 1
split과 join
let names = "Bilbo, Gandalf, Nazgul";
let arr = names.split(",");
for(let name of arr) {
alert(`${name}에게 보내는 메시지`); // Bilbo에게 보내는 메시지
*split("")를 하면 글자단위로 쪼갤 수 있다.
arr.join(glue)은 split과 반대 역할을 하는 메서드이다.
let arr = ["Bilbo", "Gandalf", "Nazgul"];
let str = arr.join(";"); // 배열 요소 모두를 ;를 사용해 하나의 문자열로 합친다.
alert(str); //Bilbo;Gandalf;Nazgul
reduce와 reduceRight
forEach, for, for..of를 사용하면 배열 내 요소를 대상으로 반복 작업을 할 수 있다.
각 요소를 돌면서 반복 작업을 수행하고, 작업 결과물을 새로운 배열 형태로 얻으려면 map을 사용하면 된다.
reduce와 reduceRight는 배열을 기반으로 값 하나를 도출할 때 사용된다.
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
이전 함수 호출 결과는 다음 함수를 호출할 때 첫 번째 인수로 사용된다.
첫 번째 인수는 앞서 호출했던 함수들의 결과가 누적되어 저장되는 '누산기'라고 생각하면 된다.
마지막 함수까지 호출되면 이 값은 reduce의 반환 값이 된다.
iterable 객체
iterable 객체는 배열을 일반화한 객체이다. 이터러블 이라는 개념을 사용하면 어떤 객체에든 for..of 반복문을
적용할 수 있다.
배열은 대표적인 이터러블이다. 배열 외에도 다수의 내장 객체가 반복 가능하다.
Symbol.iterator
let range = {
from: 1,
to: 5
};
// 아래와 같이 for..of가 동작할 수 있도록 하는 게 목표다.
// for(let num of range) ... num=1,2,3,4,5
range를 이터러블로 만들려면 객체에 Symbol.iterator라는 메서드를 추가해 아래와 같은 일이 벌어지도록 해야한다.
- for..of가 시작되자마자 for..of는 Symbol.iterator를 호출한다
Symbol.iterator는 반드시 이터레이터를 반환해야 한다. - 이후 for..of는 반환된 객체(이터레이터)만을 대상으로 동작한다.
- for..of에 다음 값이 필요하면, for..of는 이터레이터의 next()를 호출한다.
- next()의 반환 값은 {done: Boolean, value: any}와 같은 형태여야 한다.
done=true는 반복이 종료되었음을 의미한다.
done=false일땐 value에 다음 값이 저장된다.
문자열은 이터러블이다.
for..of는 문자열의 각 글자를 순회한다.
for(let char of "test") {
// 글자 하나당 한 번 실행된다(4회 호출)
alert(char); // t, e, s, t가 차례대로 출력
}
이터러블과 유사 배열
비슷해 보이지만 아주 다른 용어 두 가지가 있다.
- 이터러블은 위에서 설명한 바와 같이 메서드 Symbol.iterator가 구현된 객체이다.
- 유사 배열은 인덱스와 length 프로퍼티가 있어서 배열처럼 보이는 객체이다.
let arrayLike = { // 인덱스와 length 프로퍼티가 있음 => 유사배열
0: "Hello",
1: "World",
length: 2
};
// Symbol.iterator가 없으므로 에러 발생
for(let item of arrayLike) {}
하지만 이터러블과 유사 배열은 대개 배열이 아니기 때문에 push, pop등의 메서드를 지원하지 않는다.
Array.from
Array.from은 이터러블이나 유사 배열을 받아 진짜 Array를 만들어준다.
이 과정을 거치면 이터러블이나 유사 배열에 배열 메서드를 사용할 수 있다.
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (메서드가 제대로 동작한다.)
(*)로 표시한 줄에 있는 Array.from은 객체를 받아 이터러블이나 유사 배열인지 조사한다.
넘겨 받은 인수가 이터러블이나 유사 배열인 경우, 새로운 배열을 만들고 객체의 모든 요소를
새로 만든 배열로 복사한다.
맵과 셋
맵
맵은 키가 있는 데이터를 저장한다는 점에서 객체와 유사하다. 다만 맵은 키에 다양한 자료형을 허용한다.
- new Map() - 맵을 만든다
- map.set(key, value) - key를 이용해 value를 저장한다.
- map.get(key) - key에 해당하는 값을 반환한다. key가 존재하지 않으면 undefined를 반환한다.
- map.has(key) - key가 존재하면 true, 존재하지 않으면 false를 반환한다.
- map.delete(key) - key에 해당하는 값을 삭제한다.
- map.clear() - 맵 안의 모든 요소를 제거한다.
- map.size - 요소의 개수를 반환한다.
맵은 키로 객체를 허용한다.
let john = {name: "John"};
// 고객의 가게 방문 횟수를 세본다고 가정하자.
let visitsCountMap = new Map();
// john을 맵의 키로 사용한다,
visitsCountMap.set(john, 123);
alert(visitsCountMap.get(john)); // 123
Object.entries: 객체를 맵으로 바꾸기
평범한 객체를 가지고 맵을 만들고 싶다면 내장 메서드 Object.entries(obj)를 활용해야 한다.
이 메서드는 객체의 키-값 쌍을 요소([key, value])로 가지는 배열을 반환한다.
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert(map.get("name")); // John
Object.fromEntries: 맵을 객체로 바꾸기
let map = new Map();
map.set("banana", 1);
map.set("orange", 2);
map.set("meat", 4);
let obj = Object.fromEntries(map);
// 맵이 객체가 되었다.
// obj = {banana: 1, orange: 2, meat: 4}
alert(obj.orange); // 2
셋
셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이다.
셋에 키가 없는 값이 저장된다.
- new Set(iterable) - 셋을 만든다. 이터러블 객체를 전달받으면 그 안의 값을 복사해 셋에 넣어준다
- set.add(value) - 값을 추가하고 셋 자신을 반환한다.
- set.delete(value) - 값을 제거한다.
- set.has(value) - 셋 내에 값이 존재하면 true, 아니면 false를 반환한다.
- set.clear() - 셋을 비운다.
- set.size - 셋에 몇 개의 값이 있는지 세준다.
let set = new Set();
let john = {name: "John"};
let pete = {name: "Pete"};
let mary = {name: "Mary"};
// 어떤 고객(john, mary)은 여러 번 방문할 수 있다.
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// 셋에는 유일무이한 값만 저장된다.
alert(set.size); // 3
for(let user of set) {
alert(user.name); // John, Pete, Mary 순으로 출력
}
위크맵과 위크셋
자바스크립트 엔진은 도달 가능한(그리고 추후 사용될 가능성이 있는)값을 메모리에 유지한다.
자료구조를 구성하는 요소도 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어
메모리에서 삭제되지 않는다.
객체의 프로퍼티나 배열의 요소, 맵이나 셋을 구성하는 요소들이 이에 해당한다.
예를 들어 배열에 객체를 하나 추가하자. 이때 배열이 메모리에 남아있는 한, 배열의 요소인 이 객체도 메모리에
남아있는다. 이 객체를 참조하는 것이 아무것도 없더라도 말이다.
let john = {name: "John"};
let array = [john];
john = null; // 참조를 null로 덮어씀.
// john을 나타내는 객체는 배열의 요소이기 때문에 가비지 컬렉터의 대상이 되지않는다.
// array[0]을 이용하면 해당 객체를 얻는 것도 가능하다.
alert(JSON.stringify(array[0]]));
맵에서 객체를 키로 사용한 경우 역시, 맵이 메모리에 있는 한 객체도 메모리에 남는다.
let john = {name: "John"};
let map = new Map();
map.set(john, "...");
john = null; // 참조를 null로 덮어씀
// john을 나타내는 객체는 맵 안에 저장되어있다.
// map.keys()를 이용하면 해당 객체를 얻는 것도 가능하다.
for(let obj of map.keys()) {
alert(JSON.stringify(obj));
}
alert(map.size);
하지만 위크맵은 일반 맵과 달리 위크맵을 사용하면 키로 쓰인 객체가 가비지 컬렉션의 대상이 된다.
위크맵
맵과 위크맵의 첫 번째 차이는 위크맵의 키가 반드시 객체여야 한다는 점이다.
원시값은 위크맵의 키가 될 수 없다.
let weakMap = new weakMap();
let obj = {};
weakMap.set(obj, "ok"); // 정상적으로 동작한다(객체 키).
// 문자열("test")은 키로 사용할 수 없다.
weakMap.set("test", "Whoops"); // Error: Invalid value used as weak map key
위크맵의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 위크맵에서 자동으로 삭제된다.
맵과 위크맵의 두 번째 차이는 위크맵은 반복 작업과 keys(), values(), entries() 메서드를 지원하지 않는다.
따라서 위크맵에선 키나 값 전체를 얻는게 불가능하다.
위크맵이 지원하는 메서드
- weakMap.get(key)
- weakMap.set(key, value)
- weakMap.delete(key)
- weakMap.has(key)
유스 케이스: 추가 데이터
외부 코드에 속한 객체를 가지고 작업을 해야 한다고 가정해보자.
이 객체에 데이터를 추가해줘야 하는데, 추가해 줄 데이터는 객체가 살아있는 동안에만 유효하다.
이럴 때 위크맵을 사용할 수 있다.
위크맵에 원하는 데이터를 저장하고, 이때 키는 객체를 사용하면 된다.
이렇게 하면 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 된다.
weakMap.set(john, "비밀문서");
// john이 사망하면, 비밀문서는 자동으로 파기된다.
유스 케이스: 캐싱
위크맵은 캐싱이 필요할 때 유용하다. 캐싱은 시간이 오래 걸리는 작업의 결과를 저장해서 연산 시간과 비용을
절약해준다. 동일한 함수를 여러번 호출해야 할 때, 최초 호출 시 반환된 값을 어딘가에 저장해 놓았다가 그 다음엔
함수를 호출하는 대신 저장된 값을 사용하는 게 캐싱이다.
만약 어떤 연산을 수행하고 그 결과를 맵에 저장할 시, 객체가 필요없어져도 맵을 사용하고 있어 cache를 수동으로
청소해 줘야 한다. 하지만 위크맵을 사용하면 자동으로 메모리에서 삭제되기 때문에 걱정없다.
위크셋
- 위크셋은 셋과 유사하지만 객체만 저장할 수 있다.
- 셋 안의 객체는 도달 가능할 때만 메모리에서 유지된다.
- add, has, delete를 사용할 수 있고, size, keys()나 반복 작업 관련 메서드는 사용할 수 없다.
위크맵과 유사하게 위크셋도 부차적인 데이터를 저장할 때 사용할 수 있다.
위크셋엔 위크맵처럼 복잡한 데이터를 저장하지 않는다.
"예"나 "아니오"같은 간단한 답변을 얻는 용도로 사용된다. 물론 위크셋에 저장되는 값들은 객체이다.
let visitedSet = new WeakSet();
let john = {name: "John"};
let pete = {name: "Pete"};
let mary = {name: "Mary"};
visitedSet.add(john); // John이 사이트를 방문한다.
visitedSet.add(pete); // 이어서 Pete가 사이트를 방문한다.
visitedSet.add(john); // 이어서 John이 다시 사이트를 방문한다.
// visitedSet엔 두 명의 사용자가 저장된다.
// John의 방문 여부 확인하기
alert(visitedSet.has(john)); // true
// Mary의 방문 여부 확인하기
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet에서 john을 나타내는 객체가 자동으로 삭제된다.
Object.keys, values, entries
순회에 필요한 메서드들이 있다.
하지만 커스텀 자료구조를 대상으로 순회를 해야 한다면 이 메서드들을 쓰지 못하고 직접 구현해야 한다.
keys(), values(), entries()를 사용할 수 있는 자료구조는 다음과 같다.
- Map
- Set
- Array
Object.keys, values, entries
일반 객체엔 다음과 같은 메서드를 사용할 수 있다.
- Object.keys(obj) - 객체의 키만 담은 배열을 반환한다.
- Object.values(obj) - 객체의 값만 담은 배열을 반환한다.
- Object.entries(obj) - [키, 값] 쌍을 담은 배열을 반환한다.
<!> Object.keys, values, entries는 심볼형 프로퍼티를 무시한다.
객체 변환하기
객체엔 map, filter 같은 배열 전용 메서드를 사용할 수 있다.
하지만 Object.entries와 Object.fromEntries를 순차적으로 적용하면 객체에도 배열 전용 메서드를 사용할 수 있다.
- Object.entries(obj)를 사용해 객체의 키-값 쌍이 요소인 배열을 얻는다.
- 1에서 만든 배열에 map등의 배열 전용 메서드를 적용한다.
- 2에서 반환된 배열에 Object.fromEntries(array)를 적용해 배열을 다시 객체로 되돌린다.
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
let doublePrices = Object.fromEntries(
// 객체를 배열로 변환해서 배열 전용 메서드인 map을 적용하고 fromEntries를 사용해
// 배열을 다시 객체로 되돌린다.
Object.entries(prices).map([key, value]) => [key, value * 2])
);
alert(doublePrices.meat); // 8
구조 분해 할당
개발을 하다 보면 함수에 객체나 배열을 전달해야 하는 경우가 생긴다.
이럴 때 객체나 배열을 변수로 '분해'할 수 있게 해주는 특별한 문법인 구조 분해 할당을 사용할 수 있다.
이 외에도 함수의 매개변수가 많거나 매개변수 기본값이 필요한 경우 등에서 구조 분해는 그 진가를 발휘한다.
배열 분해하기
// 이름과 성을 요소로 가진 배열
let arr = ["Bora", "Lee"]
// 구조 분해 할당을 이용해
// firstName엔 arr[0]을
// surname엔 arr[1]을 할당하였다.
let [firstName, surname] = arr;
alert(firstName); // Bora
alert(surname); // Lee
// 반환 값이 배열인 메서드를 활용해도 좋다.
let [firstName, surname] = "Bora Lee".split(' ');
쉼표를 사용하여 요소 무시하기
// 두 번째 요소는 필요하지 않음
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(title); // Consul
변수 교환 트릭
let guest = "Jane";
let admin = "Pete";
// 변수 guest엔 Pete, 변수 admin엔 Jane이 저장되도록 값을 교환함
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane(값 교환)
'...'로 나머지 요소 가져오기
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// `rest`는 배열이다.
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
기본 값
할당하고자 하는 변수의 개수가 분해하고자 하는 배열의 길이보다 크더라도 에러가 발생하지 않는다.
할당할 값이 없으면 undefined로 취급되기 때문이다.
= 를 이용하면 할당할 값이 없을 때 기본으로 할당해 줄 값인 '기본값'을 설정할 수 있다.
// 기본값
let [name = "Guest", surname = "Anonymous", etc] = ["Julius"];
alert(name); // Julius
alert(surname); // Anonymous
alert(etc); // undefined
복잡한 표현식이나 함수 호출도 기본값이 될 수 있다.
객체 분해하기
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
// let {title: t, width: w, height: h} = options;
alert(title); // Menu
// alert(t);
alert(width); // 100
// alert(w);
alert(height); // 200
// alert(h);
Date 객체와 날짜
객체 생성하기
new Date(milliseconds)
UTC 기준(UTC +0) 1970년 1월 1일 0시 0분 0초에서 milliseconds 밀리초(1/1000 초) 후의 시점이 저장된 Date 객체가
반환된다.
// 1970년 1월 1일 0시 0분 0초(UTC +0)를 나타내는 객체
let Jan01_1970 = new Date(0);
alert(Jan01_1970);
// 1970년 1월 1일의 24시간 후는 1970년 1월 2일(UTC +0)임
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert(Jan02_1970);
1970년의 첫날을 기준으로 흘러간 밀리초를 나타내는 정수는 타임스탬프라고 부른다.
1970년 1월 1일 이전 날짜에 해당하는 타임스탬프 값은 음수이다.
new Date(year, month, date, hours, minutes, seconds, ms)
첫 번째와 두 번쨰 인수만 필수값이다.
- year는 반드시 네 자리 숫자여야 한다.
- month는 0(1월)부터 11(12월) 사이의 숫자여야 한다.
- date는 일을 나타내는데, 값이 없는 경우엔 1일로 처리된다.
- hours/minutes/seconds/ms에 값이 없는 경우엔 0으로 처리된다.
날짜 구성요소 얻기
- getFullYear() : 연도(네 자릿수)를 반환한다.
- getYear()는 두 자릿수 연도를 반환하는 경우도 있기 때문에 절대 사용해선 안된다. - getMonth()
- getDate() : 일을 반환한다.
- getHours(), getMinutes(), getSeconds(), getMilliseconds()
- getDay() : 일요일을 나타내는 0부터 토요일을 나타내는 6까지의 숫자중 하나를 반환한다.
- getTime() : 주어진 일시와 1970년 1월 1일 00시 00분 00초 사이의 간격(밀리초 단위)인 타임스탬프를 반환한다.
- getTimezoneOffset() : 현지 시간과 표준 시간의 차이(분)을 반환한다.
JSON과 메서드
네트워크를 통해 객체를 어딘가에 보내거나 로깅 목적으로 객체를 출력해야 한다면 객체를 문자열로 전환해야 한다.
JSON.stringify
- JSON.stringify - 객체를 JSON으로 바꿔준다.
- JSON.parse - JSON을 객체로 바꿔준다.
let student = {
name: "John",
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
wife: null
};
let json = JSON.stringify(student);
alert(typeof json); // 문자열
alert(json);
/* JSON으로 인코딩된 객체:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"wife": null
}
*/
JSON으로 인코딩된 객체는 일반 객체와 다른 특징을 보인다.
- 문자열은 큰따옴표로 감싸야 한다. JSON에선 작은따옴표나 백틱을 사용할 수 없다.
- 객체 프로퍼티 이름은 큰따옴표로 감싸야 한다.
JSON.stringify의 장점 중 하나는 중첩 객체도 알아서 문자열로 바꿔준다는 점이다.
커스텀 "toJSON"
toString을 사용해 객체를 문자형으로 변환시키는 것처럼, 객체에 toJSON이라는 메서드가 구현되어 있으면 객체를
JSON으로 바꿀 수 있다. JSON.stringify는 이런 경우를 감지하고 toJSON을 자동으로 호출해준다.
let room = {
number: 23
};
let meetup = {
title: "Conference",
date: new Date(Date.UTC(2017, 0, 1)),
room
};
alert(JSON.stringify(meetup));
/*
{
"title": "conference",
"date": "2017-01-01T00:00:00.000Z", // (1)
"room": {"number":23} // (2)
}
*/
Date 객체의 내장 메서드 toJSON이 호출되면서 date의 값이 문자열로 변환된다.(1)
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
alert(JSON.stringify(room)); // 23
alert(JSON.stringify(meetup));
/*
{
"title": "conference",
"room": 23
}
*/
위와 같이 toJSON은 JSON.stringify(room)을 직접 호출할 때도 사용할 수 있고, room과 같은 중첩객체에도 구현하여
사용할 수 있다.
JSON.parse
let json = `{
name: "John", // 실수 1: 프로퍼티 이름을 큰따옴표로 감싸지 않았다.
"surname": 'Smith', // 실수 2: 프로퍼티 값은 큰따옴표로 감싸야 하는데, 작은따옴표로 감쌌다.
'isAdmin': false, // 실수 3: 프로퍼티 키는 큰따옴표로 감싸야 하는데, 작은따옴표로 감쌌다.
"birthday": new Date(2000, 2, 3), // 실수 4: "new"를 사용할 수 없다.
"friends": [0,1,2,3] // ok
}`;
JSON은 주석도 지원하지 않는다. 주석을 추가하면 유효하지 않은 형식이 된다.
'Web > Vanilla JS' 카테고리의 다른 글
Modern Javascript / 함수 심화학습 (0) | 2021.07.08 |
---|---|
Modern Javascript / 코드 품질 - Chrome으로 디버깅 (0) | 2021.07.07 |
Modern Javascript / 객체: 기본 (0) | 2021.06.29 |
Modern Javascript / 자바스크립트 기본 (0) | 2021.06.28 |