자바스크립트는 원시값을 다루는 작업에 객체처럼 메서드를 사용하여 작업을 수월하게 하고자 했다. 단, 원시값은 객체처럼 무겁지 않고, 가능한 한 빠르고 가볍게 유지해야한다는 조건이 필요했다.
이를 해결하기 위해서 원시값에도 객체처럼 메서드를 호출 할 수 있는 방법을 아래와 같이 만들었다.
- 원시값은 그대로 남겨두어 단일 값 형태를 유지한다.
문자열
,숫자
,불린
,심볼
의 메서드와 프로퍼티에 접근할 수 있도록 언어 차원에서 허용했다.- 원시값이 앞 서 말한 타입의 메서드나 프로퍼티에 접근하려 하면 추가 기능을 제공해주는 특수한 객체,
원시 래퍼 객체(object wrapper)
를 만들어 준다.- 래퍼 객체는 작업이 끝나면 삭제된다.
래퍼 객체는 원시값의 타입에 따라 종류가 다양하고, 래퍼 객체 마다 제공하는 메서드 역시 다르다.
String
Number
Boolean
Symbol
예를 들어, 인수로 받은 문자열의 모든 글자를 대문자로 바꿔주는 str.toUpperCase()
메서드를 살펴보자.
let strExample = "Hello";
alert( strExample.toUpperCase() ); // HELLO
해당 메서드가 호출될 때 내부적으로 아래와 같이 동작한다.
- 인수로 받은 문자열은 원시값이므로 원시값의 프로퍼티(toUpperCase)에 접근하는 순간, 래퍼 객체가 만들어진다. 래퍼 객체는 문자열의 값을 알고 있고,
toUpperCase()
와 같은 메서드를 가지고 있다.- 메서드가 실행되고, 새로운 문자열이 반환된다.
- 래퍼 객체는 파괴되고, 원시값만 남는다.
이러한 내부 프로세스를 통해 원시값을 가볍게 유지하면서 메서드를 호출하여 작업을 할 수 있다.
참고로, 자바스크립트 엔진은 위 프로세스의 최적화에 많은 신경을 쓴다. 래퍼 객체를 만들지 않고도 마치 래퍼 객체를 생성한 것처럼 동작하게끔 해준다.
모던 자바스크립트는 숫자를 나타내는 두 가지 자료형을 지원한다.
- 일반적인 숫자형은 64비트 형식의 IEEE-754에 저장된다.
- 2^53 이상이거나 -2^53 이하의 임의의 길이를 가진 정수는 BigInt형 숫자로 나타낼 수 있다.
숫자는 일반적인 방식과 지수형, 두 가지 방식으로 변수에 할당할 수 있다.
큰 숫자와 작은 숫자를 입력하는 예시를 살펴보자.
// 큰 숫자 일반적인 방식
let billion = 1000000000;
// 큰 숫자 지수표현 방식
let billion = 1e9;
// 작은 숫자 일반적인 방식
let ms = 0.000001;
// 작은 숫자 지수표현 방식
let ms = 1e-6;
또, 16진수와 2진수, 8진수로 변수에 할당할 수도 있다.
// 16진수
let hexNum = 0xff;
let hexNum = 0xFF;
// 2진수
let biNum = 0b11111111
// 8wlstn
let ocNum = 0o377;
숫자값이 할당된 변수에 num.toString(base)
메서드를 사용하면 해당 값을 base진법으로 표현한 후, 이를 문자열로 반환해준다. base는 기본값이 10으로 2에서 36까지 지정할 수 있다.
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
어림수 관련 내장 함수들도 있다.
Math.floor
: 소숫점 첫 째 자리에서 내린다.
- ex) 3.1 => 3, -1.1 => -2
Math.ceil
: 소숫점 첫 째 자리에서 올린다.
- ex) 3.1 => 4, -1.1 => -1
Math.round
: 소숫점 첫 째 자리에서 반올림한다.
- ex) 3.1 => 3, 3.6 => 4, -1.1 => -1
Math.trunc
: 소수부를 무시한다.
- ex) 3.1 => 3, -1.1 => -1
이를 활용해 원하는 숫자를 소숫점 자릿수까지 남길 수 있다.
예를 들어, 1.23456을 1.23로 만들어보자. 두 가지 방식을 사용할 수 있다.
곱하고 나누기
let num = 1.23456;
Math.floor( num * 100) / 100 ); // 1.23
toFixed(n)
메서드 사용하기. 이 메서드는 소숫점 n번 째 자리까지 가장 가까운 값으로 올리거나 내리고 문자열을 반환해준다. 또, 소수부의 길이가 인수보다 작으면 끝에 0이 추가된다.
let num = 1.23456;
num.toFixed(2); // "1.23"
num.toFixed(8); // "1.23456000"
// 단항 연산자 +로 문자열을 숫자형으로 바꾸기
+num.toFixed(2); // 1.23
자바스크립트에서 숫자는 내부적으로 64비트 형식의 IEEE-754로 표현되므로, 숫자를 저장하려면 정확히 64비트가 필요하다. 64비트 중 52비트는 숫자를 저장하는데 사용되고, 11비트는 소숫점 위치를, 1비트는 부호를 저장하는데 사용된다.
숫자가 너무 커져서 64비트 공간을 넘치면 Infinity
로 처리된다.
alert( 1e500 ); // Infinity
또, 정밀도 손실(loss of precision)도 일어난다.
alert( 0.1 + 0.2 == 0.3 ); // false
왜 이러한 일들이 발생할까?
이유는 숫자가 0과 1로 이루어진 이진수로 변환되어 연속된 메모리 공간에 저장되기 때문이다. 예를 들어, 0.1과 0.2같은 분수는 이진법으로 표현하면 무한 소수가 된다.
이를 해결하기 위해, IEEE-754에서는 가능한 가장 가까운 숫자로 반올림하는 방법을 사용한다. 반올림을 사용하면 어쩔 수 없이 작은 정밀도 손실이 일어나게 된다.
정밀도 손실로 숫자 계산에서 오차가 발생하는 상황을 해결하는 방법은 없을까? 가장 신뢰할 수 있는 방법은 앞 서 배운 toFixed()
메서드를 사용하여 어림수를 만드는 것이다.
let sum = 0.1 + 0.2;
alert( +sum.toFixed(1) == 0.3 ); // true
자바스크립트에는 두 종류의 특수 숫자 값이 존재한다.
Infinity
,-Infinity
: 그 어떤 숫자보다 크거나 작은 값NaN
: 숫자가 아님을 나타내는 값
두 특수 숫자는 숫자형에 속하지만 정상적인 숫자가 아니기 때문에, 특별한 함수 isNaN()
과 isFinite()
가 존재한다.
// isNaN(value) : 인수를 숫자로 변환한 다음 NaN인지 테스트한다.
alert( isNaN(NaN) ); // true
alert( isNaN("Hi") ); // true
// isFinite(value) : 인수를 숫자로 변환하고 변환한 숫자가 NaN, Infinity, -Infinity가 아닌, 일반 숫자인 경우 true를 반환한다.
alert( isFinite("15") ); // true
alert( isFinite("Hi") ); // false
alert( isFinite(Infinity) ); // false
단항 덧셈 연산자 +
와 Number()
를 사용하여 숫자형으로 변형할 때, 피연산자가 숫자가 아니면 형 변환이 실패한다.
alert( +"100px" ); // NaN
실무에서는 100px, 12pt와 같이 숫자와 단위를 함께 쓰는 경우가 흔하다. 따라서, 숫자만 추출하는 방법이 필요한데, 이 때 쓸 수 있는 내장함수가 바로 parseInt
와 parseFloat
이다.
두 함수는 불가능할 때까지 문자열에서 숫자를 읽는다. 왼쪽부터 숫자를 읽는 도중 오류(숫자가 아닌 값을 부분을 만나면)가 발생하면, 이미 수집된 숫자를 parseInt
는 정수로, parseFloat
는 부동 소숫점 숫자로 반환한다.
parseInt("100px"); // 100
parseFloat("12.5em"); // 12.5
parseInt("12.3"); // 12
parseFloat("12.3.4"); // 12.3
parseInt("a123"); // NaN
parseInt(str, radix)
의 두 번째 매개변수 radix
에 원하는 진수를 지정해 줄 수 있다. 이를 통해, 16진수 문자열, 2진수 문자열 등을 파싱할 수 있다.
parseInt("0xff", 16); // 255
parseInt("ff", 16); // 255
parseInt("2n9c", 36); // 123456
자바스크립트에서 제공하는 내장 객체 Math에는 다양한 수학 관련 함수와 상수 프로퍼티가 있다.
Math.random()
: 0과 1 사이의 난수를 반환한다.(1은 제외)Math.max(a, b, c, ...)
,Math.min(a, b, c, ...)
: 인수 중 최대값 혹은 최소값을 반환한다.Math.pow(x, n)
: x를 n번 거듭제곱한 값을 반환한다.
Math.random(); // 0.3123123
Math.max(3, 5, -10, 0, 1); // 5
Math.min(1, 2); // 1
Math.pow(2, 10); // 1024
이 외에도 다양한 함수가 있다.
자바스크립트에서 텍스트 형식의 데이터는 길이에 상관없이 문자열 형태로 저장된다.
문자열은 페이지 인코딩 방식과 상관없이 항상 UTF-16 형식을 따른다.
""
: 큰 따옴표''
: 작은 따옴표``
: 백틱
위의 따옴표들은 모두 문자열을 만들 수 있다. 다만, 백틱은 조금 더 특별한 기능이 추가되어 있다. 백틱은 표현식을 ${}
로 감싸서 문자열 안에 삽입할 수 있게 해준다. 또, 문자열을 여러 줄에 걸쳐서 작성할 수 있다. 이러한 방식을 템플릿 리터럴(template literal)이라 한다.
// 백틱 문자열 중간에 표현식 넣기
alert( `1 + 2 = ${1 + 2}` ); // 1 + 2 = 3
// 백틱을 사용해서 문자열을 여러 줄에 작성하기
let guestList = `손님:
John
Pete
Mary
`;
자바스크립트에는 이스케이프 문자\
로 시작하는 다양한 제어 문자들이 있다.
\n
: 줄 바꿈\r
: 캐리지 리턴. 단독으로 사용하는 경우는 없음.\', \"
: 따옴표\\
: 역슬래시\t
: 탭\xXX
: 16진수 유니코드XX
로 표현한 유니코드 글자\uXXXX
: UTF-16 인코딩 규칙을 사용하는 16진수 코드XXXX
로 표현한 유니코드 기호\u{XXXXX...}
: UTF-32로 표현한 유니코드 기호
위 제어문자들의 예시를 살펴 보자.
// 줄 바꿈 제어문자 예시
let str1 = "Hello\nWorld"; // Hello
// World
// 유니코드 제어문자 예시
alert( "\u00A9" ); // ©
alert( "\u{1F60D}" ); // 😍
// 따옴표 제어 문자
alert( 'I\'m the Walrus' ); // I'm the Walrus
alert( `My\n`.length ); // 3
문자열의 특정 글자에는 인덱스와 대괄호 혹은 str.charAt()
메서드로 접근할 수 있다. 위치는 0부터 시작한다.
let str = "Hello";
// 대괄호 사용
str[0]; // H
str[1000]; // 반환할 글자가 없으면 undefined 반환
// 메서드 사용
str.charAt(0); // H
str.charAt(1000); // 반환할 글자가 없으면 "" 빈 문자열 반환
for of
반복문을 통해 문자열을 구성하는 글자들을 대상으로 반복 작업을 할 수 있다.
for (let char of "hello") {
alert(char); // h, e, l, l, o
}
문자열은 수정할 수 없다. 수정을 하려고 하면 에러가 발생한다.
수정하고싶다면 수정하고 싶은 부분을 포함한 새로운 문자열을 만들어 사용할 수 있다.
let str = "Hi";
str[0] = "h"; // Cannot assign to read only property '0' of string 'Hi'
str = "h" + str[1];
alert( str ); // hi
toLowerCase()
와 toUpperCase()
메소드를 사용해서, 문자열의 대소문자를 변경할 수 있다. 인덱스를 활용해 지정한 글자 하나만 바꾸어 해당 글자를 반환할 수도 있다.
"Interface".toUpperCase(); // INTERFACE
"Interface".toLowerCase(); // interface
"Interface"[0].toLowerCase(); // i
자바스크립트에서 부분 문자열을 찾는 방법은 여러 가지이다.
첫 번째 방법은 str.indexOf(substr)
메서드이다. 해당 메서드는 부분 문자열substr
이 어디에 위치하는지 찾아준다. 원하는 부분 문자열을 찾으면 위치를 반환하고, 못찾으면 -1을 반환한다. 원하는 부분 문자열이 여러 개이면 가장 먼저 발견한 위치를 반환한다.
또, 두 번째 매개변수 pos
를 사용하여, 검색을 해당 pos
위치 부터 시작할 수도 있다.
let str = "Widget with id";
str.indexOf("Widget"); // 0
str.indexOf("widget"); // -1
str.indexOf("id"); // 1
str.indexOf("id", 2); // 12. 반환되는 인덱스는 처음부터이다.
전체 부분 문자열을 대상으로 무언가를 하고 싶다면 반복문 안에서 indexOf
를 사용하면 된다.
let str = "As sly as a fox, as strong as an ox";
let target = "as";
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
alert( `위치: ${foundPos}` );
pos = foundPos + 1;
}
// 짧은 코드 버전
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert( `위치: ${pos}` );
}
str.lastIndexOf(substr, pos)
메서드도 있다. 위 메소드와 동일하게 동작하지만, 문자열 끝에서부터 부분 문자열을 찾는다는 점만 다르다.
str.includes(substr, pos)
메서드는 문자열에 부분 문자열 substr
이 있으면 true
, 없으면 false
를 반환한다.
부분 문자열의 위치 정보보단, 포함여부를 알고 싶을 때 사용하는 메서드이다.
"Widget with id".includes("Widget"); // true
"Hello".includes("Bye"); // false
// 인덱스를 두 번째 매개변수 pos에 넘겨, 해당 위치부터 부분 문자열을 검색
"Widget".includes("id", 3); // false
str.startsWith()
와 str.endsWith()
는 메서드 이름 그대로, 문자열이 매개변수로 넘긴 부분 문자열로 시작하는지 혹은 끝나는지 여부를 확인할 때 사용한다.
"Widget".startsWith("Wid"); // true
"Widget".endsWith("get"); // true
자바스크립트에는 부분 문자열 추출과 관련된 세 가지 메서드가 있다.
str.slice(start, [, end])
는 인덱스로 지정한 start
부터 end-1
까지의 부분 문자열을 반환한다.
두 번째 인수가 생략된 경우, 명시한 위치부터 문자열 끝까지를 반환한다.
let str = "stringfy";
str.slice(0, 5); // strin
str.slice(0, 1); // s
str.slice(2); // ringfy
str.substring(start, [, end]
메서드는 start
와 end
사이에 있는 문자열을 반환한다. start
가 end
보다 커도 된다.
let str = "stringfy";
str.substring(2, 6); // ring
str.substring(6, 2); // ring
문자열을 비교할 때는 알파벳 순서를 기준으로 글자끼리 비교가 이뤄지며, 다음의 규칙들을 따른다.
- 소문자는 대문자보다 항상 크다.
- 발음 구별 기호가 붙은 문자는 알파벳 순서 기준을 따르지 않는다.
모든 문자열은 UTF-16을 사용해서 인코딩되는데, 모든 문자가 숫자 형식의 코드와 매칭된다.
str.codePointAt(pos)
메서드를 문자열에 사용해서 인덱스에 위치한 문자의 코드를 반환한다.
"z".codePointAt(0); // 122
"Z".codePointAt(0); // 90
반대로, fromCodePoint(code)
메서드를 사용해서 코드에 대응하는 글자를 만들 수 있다.
String.fromCodePoint(90); // Z
데이터를 순서대로 저장할 때 쓰는 자료구조이다.
두 가지 문법을 사용해서 배열을 만들 수 있다.
// 배열 생성자를 사용해서 빈 배열 선언
let arr = new Array();
// 배열 생성자를 사용해서 배열 선언 및 초기화
let arr = new Array("사과", "배", "기타");
// 인수로 숫자를 넣으면 해당 길이를 가지는 빈 배열을 만든다.
let arr = new Array(2);
alert( arr[0] ); // undefined
alert( arr.length ); // 2
// 대괄호를 사용해서 빈 배열 선언
let arr = [];
// 대괄호를 사용해서 배열 선언 및 초기화
let fruits = ["사과", "오렌지", "자두"];
각 배열 요소에는 0부터 시작하는 인덱스가 매겨져 있다. 이 인덱스를 통해서 다음과 같은 동작을 할 수 있다.
- 배열 내 특정 요소에 접근
- 특정 요소 수정
- 배열에 새로운 요소 추가
let fruits = ["사과", "오렌지", "자두"];
// 요소 접근
fruits[0]; // 사과
fruits[1]; // 오렌지
fruits[2]; // 자두
// 요소 수정
fruits[2] = "배";
// 새로운 요소 추가
fruits[3] = "레몬";
length
프로퍼티를 통해 배열의 길이를 알아낼 수 있다. 배열의 가장 큰 인덱스에 1을 더하면 된다.
let fruits = ["사과", "오렌지", "자두"];
fruits.length; // 3
let fruits = [];
fruits[123] = "사과";
fruits.length; // 124
배열에 무언가 조작을 하면 length
프로퍼티가 자동으로 갱신된다.
length
프로퍼티를 활용해서 배열을 초기화할 수 있다. length
값을 수동으로 증가시키면 아무 일도 일어나지 않지만, 값을 감소시키면 배열이 잘린다. 잘려진 배열은 다시 되돌릴 수 없다.
let arr = [1, 2, 3, 4, 5];
arr.length = 2;
arr; // [1, 2]
arr.length = 5;
arr[3]; // undefined
// length 프로퍼티를 활용한 배열 초기화
arr.length = 0;
문자열, 숫자, 불리언, 객체, 배열, 함수 등 다양한 데이터를 담을 수 있다.
let arr = [ "사과", { name: "용" }, true, function() { alert("안녕하세요"); } ];
alert( arr[1].name ); // 용
arr[3](); // 안녕하세요
pop()
: 배열 끝의 요소를 제거하고, 제거한 요소를 반환한다.push()
: 배열 끝의 요소를 추가한다. 요소 여러 개를 한 번에 추가할 수 있다.shift()
: 배열 앞 요소를 제거하고, 제거한 요소를 반환한다.unshift()
: 배열 앞에 요소를 추가한다. 요소 여러 개를 한 번에 추가할 수 있다.
let fruits = ["사과", "오렌지", "배"];
// pop
fruits.pop(); // 배
fruits // 사과, 오렌지
// push
fruits.push("배");
fruits // 사과, 오렌지, 배
// shift
fruits.shift(); // 사과
fruits // 오렌지, 배
// unshift
fruits.unshift("사과");
fruits // 사과, 오렌지, 배
// 여러 개 요소 push, unshift
let fruits = ["사과"];
fruits.push("오렌지", "배");
fruits.unshift("파인애플", "레몬");
fruits // 파인애플, 레몬, 사과, 오렌지, 배
배열은 특별한 종류의 객체이다. 배열에 typeof
연산자를 사용하면 객체형을 반환한다.
객체에 대괄호와 키를 사용해서 프로퍼티에 접근하는 것처럼, 배열도 대괄호와 인덱스를 사용해서 요소에 접근할 수 있다.
또, 객체가 참조에 의해 복사를 하는 것처럼 배열도 참조에 의한 복사를 한다.
let fruits = ["바나나"];
let arr = fruits;
arr === fruits; // true
arr.push("배");
alert( fruits ); // 바나나, 배
자바스크립트 엔진은 배열의 요소를 인접한 메모리 공간에 차례로 저장해 연산 속도를 높인다. 이러한 점이 일반 객체와 다른다.
for
반복문은 배열을 순회할 때 쓰는 가장 기본적인 방법이다.
let arr = ["사과", "오렌지", "배"];
for (let i = 0; i < arr.length; i++) {
alert( arr[i] );
}
for of
반복문으로도 순회할 수 있다.
for (let fruit of fruits) {
alert(fruit);
}
배열의 요소로 또 다른 배열을 넣을 수 있다. 이러한 배열을 다차원 배열이라 부른다. 보통, 행렬을 저장하는 용도로 쓰인다.
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
alert( matrix[1][1] ); // 5
배열에는 toString
메서드가 구현되어 있어, 이를 호출하면 요소를 쉼표로 구분한 문자열이 반환된다.
let arr = [1, 2, 3];
alert( String(arr) ); // "1,2,3"
push
, pop
, shift
, unshift
메서드 이외에 배열 요소 추가와 삭제 메서드가 있다.
arr.splice(index, [deleteCount, elem1, ...])
메서드는 기본적으로 요소를 삭제하는 메서드이지만, 요소 추가와 교체가 모두 가능한 메서드 이다.
첫 번째 매개변수 index
는 조작을 가할 첫 번째 요소를 가리키는 인덱스이다. 두 번째 매개변수 deletecount
는 제거하고자 하는 요소의 개수를 나타낸다. elem1, ...
는 배열에 추가할 요소를 나타낸다.
splice
는 삭제된 요소로 구성된 배열을 반환한다.
deleteCount
를 0으로 설정하면 요소를 제거하지 않으면서 새로운 요소를 추가할 수 있다.
// 요소 한 개 제거
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1);
arr; // ["I", "JavaScript"]
// 요소 여러 개 제거 후 요소 대체
let arr = ["I", "study", "JavaScript", "right", "now"];
arr.splice(0, 3, "Let's", "dance");
arr; // ["Let's", "dance", "right", "now"]
// 삭제된 요소 반환
let arr = ["I", "study", "JavaScript", "right", "now"];
let removed = arr.splice(0, 2);
removed; // ["I", "study"]
// 요소 제거하지 않고, 요소 추가
let arr = ["I", "study", "JavaScript"];
arr.splice(2, 0, "complex", "language");
arr; // ["I", "study", "complex", "language", "JavaScript"]
arr.slice([start], [end])
메서드는 start
인덱스부터 end - 1
인덱스까지의 요소를 복사한 새로운 배열을 반환한다.
인수를 넘기지 않으면, 배열의 복사본을 만들 수 있다.
let arr = ["t", "e", "s", "t"];
alert( arr.slice(1, 3) ); // ["e", "s"]
alert( arr.slice(-2) ); // ["s", "t"]
alert( arr.slice() ); // ["t", "e", "s", "t"]
arr.concat(arg1, ...)
메서드는 기존 배열에 요소를 추가해서 새로운 배열을 반환한다. 인수에는 배열이나 값이 올 수 있다.
let arr = [1, 2];
arr.concat([3, 4]); // [1, 2, 3, 4]
arr.concat([3, 4], [5, 6]); // [1, 2, 3, 4, 5, 6]
arr.concat([3, 4], 5, 6); // [1, 2, 3, 4, 5, 6]
concat()
메서드는 객체가 인자로 넘어오면 객체는 분해되지 않고 통으로 복사된다. 그런데 인자로 받은 객체에 Symbol.isConcatSpreadable
프로퍼티가 있으면 이 객체를 배열처럼 취급한다. 따라서, 객체 전체가 아닌 객체 프로퍼티의 값이 더해진다.
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
arr.concat(arrayLike); // [1, 2, {0: "something", length: 1}]
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
arr.concat(arrayLike); // [1, 2, "something", "else"]
arr.forEach(callback)
는 콜백함수를 사용해서, 배열 요소 각각에 대해 해당 콜백의 동작을 실행한다.
arr.forEach(function(item, index, array) {
// ...
});
// 배열 각 요소에 대해 alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
// 인덱스와 배열에 대한 정보까지 더해서 출력
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
참고로 인수로 넘겨준 콜백 함수의 반환값은 무시된다.
map(callback)
메서드는 배열 요소 전체를 대상으로 함수를 호출하고, 콜백 함수 호출 결과를 배열로 반환해준다.
// 문법
let result = arr.map(function(item, index, array) {});
// 각 요소의 길이를 출력하는 예시
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(
item => item.length
);
lengths; // [5, 7, 6]
배열의 각 요소를 대상으로 반복 작업을 해서 값 하나를 도출할 때 주로 사용된다.
let value = arr.reduce(function(accumulator, item, index, array) {
// accumulater : 이전 함수 호출 결과
// item : 현재 배열 요소
// index : 요소의 위치
// array : 배열
}, [initial]);
// initial(옵션) : 함수 최초 호출 시 사용되는 accumulator의 초기값
인수로 넘겨주는 콜백 함수는 배열의 모든 요소를 대상으로 차례차례 적용되는데, 적용 결과는 다음 함수 호출 시에도 사용된다.
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
result; // 15
arr.reduceRight
메서드는 동일한 기능을 하지만, 배열의 오른쪽부터 연산을 수행한다.
배열 내에서 무언가를 찾고 싶을 때 쓰는 메서드를 알아보자.
arr.indexOf(item, from)
: 배열에 인덱스from
부터 시작해서item
을 찾는다. 찾으면 해당 요소의 인덱스를 반환하고, 발견하지 못하면 -1을 반환한다.arr.lastIndexOf(item, from)
: 검색을 배열 끝에서부터 시작한다.arr.includes(item, from)
: 배열에 인댁스from
부터 시작해서item
이 있는지 검사하고, 해당 요소를 발견하면 true를 반환한다.
위 메서드들은 요소를 찾을 때 일치 연산자 ===
를 사용한다.
let arr = [1, 0, false];
arr.indexOf(0); // 1
arr.indexOf(false); // 2
arr.indexOf(null); // -1
arr.includes(1); // true
arr.find(function(item, index, array) {
// item : 함수를 호출하는 요소
// index : 요소의 인덱스
// array : 메서드를 호출한 배열
// ...
});
배열의 각 요소마다 콜백을 호출한다. 콜백 결과로 true가 반환되면 반복이 멈추고, 해당 요소를 반환한다. 조건에 해당하는 요소가 없으면 undefined를 반환한다.
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let user = users.find(item => item.id == 1);
user.name; // John
arr.findIndex()
메서드는 동일한 일을 하나, 조건에 맞는 요소를 발견하면 해당 요소의 인덱스를 반환한다. 조건에 맞는 요소가 없으면 -1이 반환된다.
filter(callback)
메서드는 조건을 충족하는 요소를 여러 개 찾아서 배열로 반환해준다.
let results = arr.filter(function(item, index, array) {});
조건을 충족하는 요소가 배열에 순차적으로 더해진다. 조건을 충족하는 요소가 없으면 빈 배열이 반환된다.
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let someUsers = users.filter(item => item.id < 3);
someUsers; // [ {id: 1, name: "John"}, {id: 2, name: "Pete"} ]
배열의 요소를 정렬해주는 메서드이다. 배열 자체가 변경된다.
let arr = [1, 2, 15];
arr.sort();
arr; // [1, 15, 2];
오름차순 결과를 기대했지만, 결과가 예상과 다르게 나왔다. 이유는 뭘까?
sort()
메서드에서 요소는 문자열로 취급되어 재정렬되기 때문이다.
기본적으로 정렬되는 기준 대신 새로운 정렬 기준을 만들려면 arr.sort([callback])
콜백 함수로 정렬 기준이 되는 함수를 넘겨주면 된다. 인수로 넘겨주는 함수는 반드시 값 두개를 비교해야 하고, 반환값도 있어야 한다.
let arr = [1, 2, 15];
arr.sort((a, b) => {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
});
arr; // [1, 2, 15]
콜백 함수의 반환 값에는 제약이 없다. 첫 번째 인수가 두 번째 인수보다 크다를 나타내려면 양수를, 첫 번째 인수가 두 번째 인수보다 작다를 나타내려면 음수를 반환하면 된다. 이 점을 활용해서 위 콜백 함수를 더 간결하게 만들 수 있다.
let arr = [1, 2, 15];
arr.sort((a, b) => a - b);
arr; // [1, 2, 15]
arr.reverse()
메서드는 배열의 요소를 역순으로 정렬시켜주는 메서드이다.
let arr = [1, 2, 3, 4, 5];
arr.reverse();
arr; // [5, 4, 3, 2, 1]
str.split(구분자)
메서드를 이용하면 구분자를 기준으로 문자열을 쪼개고 각각 배열의 요소를 집어넣어, 배열을 반환해준다.
두 번째 인수로 숫자를 전달해서, 반환받는 배열의 길이를 제한할 수 있다.
let names = "Bilbo, Gandalf, Nazgul";
let arr = names.split(", ");
arr; // ["Bilbo", "Gandalf", "Nazgul"]
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
arr; // ["Bilbo", "Gandalf"]
arr.join(구분자)
메서드를 사용하면 구분자를 사용해서, 배열 요소를 모두 합친 후 하나의 문자열로 반환한다.
let arr = ["Bilbo", "Gandalf", "Nazgul"];
let str = arr.join(";");
str; // Bilbo;Gandalf;Nazgul
자바스크립트에서 배열은 객체형에 속하므로, typeof
연산자로는 일반 객체인지 배열인지 구분할 수 없다.
이 때, 사용할 수 있는 메서드가 Array.isArray(value)
이다. value
가 배열이면 true
, 배열이 아니면 false
를 반환해준다.
Array.isArray({}); // false
Array.isArray([]); // true
sort
를 제외한 find
, filter
, map
등 함수를 호출하는 대부분의 배열 메서드는 thisArg
라는 매개변수를 옵션으로 받을 수 있다.
thisArg
는 콜백 함수의 this
가 된다.
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
// army.canJoin 호출 시 참을 반환해주는 user를 찾음
let soldiers = users.filter(army.canJoin, army);
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
이터러블은 배열을 일반화한 객체이다.
이터러블이라는 개념을 사용하면, 어떤 객체에든 for of
반복문을 적용할 수 있다. 반대로 말해서 for of
를 사용할 수 있는 객체는 이터러블이다.
배열이 대표적인 이터러블이다. 배열 외에도 다수의 내장 객체가 반복 가능하다. 문자열 역시 이터러블의 한 예이다.
배열이 아닌 객체가 있는데, 이 객체가 어떤 것들의 컬렉션을 나타내고 있는 경우에는 for of
문법을 적용할 수만 있다면 해당 컬렉션을 순회하는데 유용할 것이다.
따라서, 해당 객체를 이터러블로 만들 수 있는 방법을 살펴볼 것이다.
객체를 이터러블로 만드는 과정을 다음과 같다.
for of
를 적용하기에 적합해 보이는 객체를 만든다.- 해당 객체에
Symbol.iterator
메서드(심볼형 프로퍼티)를 추가한다.
Symbol.iterator
메서드를 추가한 이터러블에 for of
를 호출했을 때 발생하는 내부 동작을 살펴보자.
for of
가 시작되면,for of
는 객체의Symbol.iterator
메서드를 호출한다.
- 객체에 해당 특수 내장 심볼이 없으면 에러가 발생한다.
Symbol.iterator
메서드는 반드시next
메서드가 있는 객체iterator
를 반환해야 한다.- 이후
for of
는 객체iterator
만을 대상으로 동작한다.for of
에 다음 값이 필요하면,for of
는iterator
의next
메서드를 호출한다.next
메서드의 반환 값은{done: Boolean, value: any}
와 같은 형태여야 한다.done
이true
이면 반복이 종료되었음을 의미하고,false
이면value
에 다음 값이 저장된다.
직접 이터러블을 만들어보자. 이터러블이 아닌 객체 range
를 이터러블로 만들어주는 코드는 다음과 같다.
let range = {
from: 1,
to: 5
};
// 1. for of 최초 호출 시, Symbol.iterator가 호출된다.
range[Symbol.iterator] = functuon() {
// Symbol.iterator는 객체 iterator를 반환한다.
// 2. 이후 for of는 반환된 iterator만을 대상으로 동작한다
return {
current: this.from,
last: this.to,
// 3. for of 반복문에 의해 반복마다 next() 메서드가 호출된다.
next() {
// 4. next() 메서드는 값을 객체 {done:boolean, value:any} 형태로 반환한다.
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
for (let num of range) {
alert(num); // 1, 2, 3, 4, 5
}
이터러블의 핵심은 관심사의 분리(Separation of Concern, SoC)
에 있다.
- 이터러블에는 메서드
next()
가 없다.- 대신에
Symbol.iterator()
를 호출해서 만든 객체iterator
와 해당 객체의 메서드next()
에서 반복에 사용될 값을 만들어낸다.
이러한 방식으로 iterator
객체와 이터러블을 분리할 수 있다.
iterator
객체와 이터러블을 합쳐서 이터러블 자체를 iterator
로 만들면 코드가 더 간단해진다.
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.form;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, 2, 3, 4, 5
}
문자열은 배열과 마찬가지로 가장 광범위하게 쓰이는 내장 이터러블이다.
for of
는 문자열의 각 글자를 순회한다.
for (let char of "test") {
alert(char); // t, e, s, t
}
for of
를 사용했을 때와 동일한 작업을 하지만, 문자열에 iterator
를 직접 호출해서 순회해보자.
iterator
를 명시적으로 호출하는 경우는 거의 없지만, 반복 과정을 더 잘 통제할 수 있다는 장점이 있다.
예를 들어, 반복을 시작했다가 잠시 멈춰 다른 작업을 하다가 다시 반복을 시작하는 것과 같이 반복 과정을 여러 개로 쪼개는 것이 가능하다.
let str = "Hello";
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value);
}
아래 두 객체는 서로 같은 의미가 아니다.
이터러블
: 메서드Symbol.iterator
가 구현된 객체이다.유사 배열 객체
: 인덱스와length
프로퍼티가 있어서 배열처럼 보이는 객체이다.
또, 이터러블과 유사 배열은 대개 배열이 아니기 때문에 push
, pop
등의 메서드를 지원하지 않는다. 이러한 객체들을 배열처럼 다루고 싶을 때는 어떻게 할까?
Array.from(obj, [mapFn, thisArg])
메서드는 이터러블 혹은 유사 배열을 받아 새로운 배열을 만들고, 객체의 모든 요소를 새롭게 만든 배열로 복사한다.
두 번째 인수로 매핑함수를 넘겨주면 새로운 배열에 요소를 추가하기 전에 각 요소를 대상으로 연산을 수행할 수 있다.
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike);
alert( arr.pop() ); // World
let arr = Array.from(range);
arr; // [1, 2, 3, 4, 5]
let arr = Array.from(range, num => num * num);
arr; // [1, 4, 9, 16, 25]
맵은 키가 있는 데이터를 저장한다는 점에서 객체와 유사하지만, 객체와 달리 키를 문자형으로 변환하지 않아 자료형에 제약이 없다는 점이 다르다.
맵에는 다음과 같은 주요 메서드와 프로퍼티가 있다.
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 map = new Map();
map.set("1", "str1");
map.set(1, "num1");
map.set(true, "bool1");
map.get(1); // num1
map.get("1"); // str1
map.size; // 3
// 맵은 키로 객체도 허용한다.
let john = { name: "John" };
let visitsCountMap = new Map();
visitsCountMap.set(john, 123);
visitsCountMap.get(john); // 123
맵에는 일반 객체처럼 대괄호 표기법을 사용해 키에 접근하거나, 프로퍼티를 추가하는 것은 좋지 않다. 대신에 맵 전용 메서드 set
, get
을 사용해야한다.
맵은 set
메서드를 호출할 때마다 맵 자신이 반환된다. 이를 활용해서 체이닝을 할 수도 있다.
map.set("1", "str1").set(1, "num1").set(true, "bool1");
각 요소가 키-값 쌍인 배열이나 이터러블을 맵에 전달해서 초기화할 수 있다.
// 배열로 초기화
let map = new Map([
["1", "str1"],
[1, "num1"],
[true, "bool1"]
]);
map.get("1"); // str1
// 객체로 초기화.
// Object.entries(obj)은 객체를 키-값 쌍을 요소로 가지는 배열로 반환
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
map.get("name"); // John
다음의 메서드를 사용해서 맵의 각 요소에 반복 작업을 할 수 있다.
map.keys()
: 각 요소의 키를 모아서 이터러블을 반환한다.map.values()
: 각 요소의 값을 모아서 이터러블을 반환한다.map.entries()
: 각 요소의 키와 값을 한쌍으로 배열로 만들고, 이를 모아서 이터러블로 반환한다.
맵은 값이 삽입된 순서대로 순회를 한다.
또, 맵은 배열과 유사하게 내장 메서드 forEach()
를 지원한다.
let recipeMap = new Map([
["cucumber", 500],
["tomatoes", 350],
["onion", 50]
]);
// 키를 대상으로 순회
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 값을 대상으로 순회
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 키-값 쌍을 대상으로 순회
for (let entry of recipeMap.entries()) {
alert(entry); // [cucumber, 500] ...
}
// 맵 자체의 순회는 map.entries()와 같은 동작을 한다.
for (let entry of recipeMap) {
alert(entry); // [cucumber, 500] ...
}
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 ...
});
위에서 객체를 맵으로 바꾸는 방법을 알아보았다.
반대로, Object.fromEntries()
메서드를 사용해서 맵을 객체로 바꾸어 보자. 이 메서드는 각 요소가 키-값 쌍인 배열을 객체로 바꾸어준다.
let map = new Map();
map.set("banana", 1);
map.set("orange", 2);
map.set("meat", 4);
let obj = Object.fromEntries(map.entries());
obj; // { banana: 1, orange: 2, meat: 4 }
// 같은 동작
let obj = Object.fromEntries(map);
obj; // { banana: 1, orange: 2, meat: 4 }
셋은 중복을 허용하지 않는 값을 모아놓은 컬렉션이다. 셋에는 키가 없고, 값만 저장된다.
셋의 주요 메서드와 프로퍼티는 다음과 같다.
new Set(iterable)
: 셋을 만든다. 이터러블을 전달받으면, 그 안의 값을 복사해서 셋에 넣어준다.set.add(value)
: 값을 추가하고, 셋 자신을 반환한다. 셋 내에 동일한 값이 있으면set.add(value)
를 호출해도 동작하지 않는다. 셋은 중복값을 허용하지 않기 때문이다.set.delete(value)
: 값을 제거한다. 셋에 값이 있어서 제거에 성공하면true
, 실패하면false
를 반환한다.set.has(value)
: 셋 내에value
가 존재하면true
, 존재하지 않으면false
를 반환한다.set.clear()
: 셋을 비운다.set.size
: 셋의 요소 갯수를 반환한다.
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
set.size; // 3
for (let user of set) {
alert(user.name); // John, Pete, Mary
}
for of
나 forEach
를 사용해서 셋의 값을 대상으로 반복 작업을 수행할 수 있다.
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) {
alert(value); // oranges, apples, bananas
}
set.forEach((value, valueAgain, set) => {
alert(value);
});
위 forEach()
메서드에서 콜백 함수에 쓰인 두 번째 인자는 첫 번째 인자와 값이 같다. 이는 맵과의 호환성 때문이다. 이렇게 구현해 놓으면 맵을 셋으로 혹은 셋을 맵으로 교체하기 쉽다.
셋도 맵과 마찬가지로 반복 작업을 위한 메서드들이 있다.
set.keys()
: 셋 내의 모든 값을 포함하는 이터러블을 반환한다.set.values()
: 셋 내의 모든 값을 포함하는 이터러블을 반환한다. 맵과의 호환성을 위해 만들어진 메서드이다.set.entries()
: 셋 내의 각 값을 이용해 만든 값-값 쌍 배열을 포함하는 이터러블을 반환한다. 맵과의 호환성을 위해 만들어진 메서드이다.
맵에서 객체를 키로 사용한 경우에 맵이 메모리에 있는 한 객체를 참조하는 것이 아무것도 없어도, 객체가 가비키 컬렉터의 대상이 되지 않는다. 이는 사용하지 않는 메모리의 낭비가 발생한다.
하지만 위크맵과 위크셋을 사용하면 메모리를 절약할 수 있다.
위크맵의 키는 반드시 객체여야 한다. 원시값은 위크맵의 키가 될 수 없다.
위크맵의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 위크맵에서 자동으로 삭제된다.
// 위크맵 선언 예시
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok");
// 위크맵에서 객체 삭제
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null;
// john을 나타내는 객체는 메모리에서 지워짐
위 예시에서 john을 나타내는 객체는 오로지 위크맵의 키로만 사용되고 있으므로, 참조를 덮어쓰게 되면 이 객체는 위크맵과 메모리에서 자동으로 삭제된다.
위크맵은 반복 작업과 keys()
, values()
, entries()
메서드를 지원하지 않는다. 따라서, 위크맵에서는 키나 값 전체를 얻는 것이 불가능하다.
위크맵이 지원하는 메서드는 다음과 같다.
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
외부 코드에 속한 객체를 가지고 작업해야 한다고 가정해보자. 이 객체에 데이터를 추가해줘야 하는데, 추가해 줄 데이터는 객체가 살아있는 동안에만 유효한 상황이다. 이럴 때, 위크맵을 사용할 수 있다.
위크맵에 원하는 데이터를 저장하고, 키로 객체를 사용한다. 이런 방식을 통해 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 된다.
weakMap.set(john, "비밀문서");
// john이 제거되면, 비밀문서는 자동으로 파기된다.
좀 더 구체적인 예시로, 사용자의 방문 횟수를 세어주는 기능을 구현해보자.
// 맵을 사용한 구현
let visitsCountMap = new Map(); // 맵에 사용자의 방문 횟수를 저장함
// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
let john = { name: "John" };
countUser(john); // John의 방문 횟수를 증가시킵니다.
// John의 방문 횟수를 셀 필요가 없어지면 아래와 같이 john을 null로 덮어씁니다.
john = null;
이처럼 맵을 사용해서 구현하면 객체를 가리키는 참조가 없어져도 가비지 컬렉터에 의해 자동으로 삭제되지 않으므로, 데이터를 손수 지워줘야 한다. 이렇게 수동으로 데이터를 비워주는 방식은 비효율적이다.
위크맵을 사용하면 객체가 도달 가능하지 않은 상태가 되면 자동으로 메모리에서 삭제되기 때문에, 수동으로 데이터를 지워줄 필요가 없다. 키에 대응하는 값이 자동으로 가비지 컬렉션의 대상이 되기 때문이다.
// 위크맵을 사용한 구현
let visitsCountMap = new WeakMap(); // 위크맵에 사용자의 방문 횟수를 저장함
// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
위크맵은 캐싱이 필요할 때 유용하다. 캐싱은 시간이 오래 걸리는 작업의 결과를 저장해서 다음 번의 연산 시간과 비용을 절약해주는 기법이다.
아래에서 맵과 위크맵을 사용한 캐싱 예시를 비교해보자.
// 맵을 사용한 구현
let cache = new Map();
// 연산을 수행하고 그 결과를 맵에 저장
function process(obj) {
if (!cache.has(obj)) {
let result = obj;
cache.set(obj, result);
}
reurn cache.get(obj);
}
let obj = {
// ...
};
let result1 = process(obj);
let result2 = process(obj); // 두 번 째 호출 때는 맵에 저장된 결과를 사용
// 객체가 쓸모없어지면 null로 덮어 씀
obj = null;
// 데이터가 자동으로 삭제되지 않아 메모리 누수
alert(cache.size); // 1
// 위크맵을 사용한 구현
let cache = new WeakMap();
// 연산을 수행하고 그 결과를 위크맵에 저장합니다.
function process(obj) {
if (!cache.has(obj)) {
let result = /* 연산 수행 */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
let obj = {/* ... 객체 ... */};
let result1 = process(obj);
let result2 = process(obj);
// 객체가 쓸모없어지면 아래와 같이 null로 덮어쓴다.
obj = null;
// 위크맵에서는 obj가 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제됨
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않음
위크셋은 셋과 유사하지만 객체만 저장할 수 있다는 점이 다르다. 원시값은 저장할 수 없다.
위크셋안의 객체는 도달 가능하지 않으면 메모리에서 삭제된다.
위크셋도 마찬가지로 반복 작업 관련 메서드를 사용할 수 없다.
위크셋이 지원하는 메서드는 다음과 같다.
weakSet.add()
weakSet.delete()
weakSet.has()
위크맵과 유사하게 위크셋도 부차적인 데이터를 저장할 때 사용할 수 있다. 다만, 위크셋에는 위크맵처럼 복잡한 데이터를 저장하지 않는다. 대신 예 혹은 아니오 같은 간단한 답변을 얻는 용도로 사용된다.
위크셋을 사용해서 사용자의 사이트 방문 여부를 추적하는 예시를 살펴보자.
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.has(john); // true
visitedSet.has(mary); // false
// John의 방문 여부가 삭제됨
john = null;
keys()
, values()
, entries()
메서드들은 맵과 셋, 배열에서 순회에 사용하는 메서드이다.
일반 객체에도 해당 메서드들이 있긴하지만, 문법에 차이가 있다.
Object.keys(obj)
: 객체의 키만 담은 배열을 반환한다.Object.values(obj)
: 객체의 값만 담은 배열을 반환한다.Object.entries(obj)
: 키-값 쌍 배열을 담은 배열을 반환한다.
let user = {
name: "John",
age: 30
};
Object.keys(user); // ["name", "age"]
Object.values(user); // ["John", 30]
Object.entries(user); // [ ["name", "John"], ["age", 30] ]
// 값을 순회하는 예시
for (let value of Object.values(user)) {
alert(value); // Violet, 30
}
위 메서드들은 for in
반복문처럼 키가 심볼형인 프로퍼티를 무시한다.
객체에는 map()
과 filter()
같은 배열 전용 메서드 역시 사용할 수 없다.
하지만 Object.entries()
와 Object.fromEntries()
를 순차적으로 적용하면 객체에도 배열 전용 메서드를 사용할 수 있다.
let prices = {
banana: 1,
orange: 2,
meat: 4
};
let doublePrices = Object.fromEntries( // 배열을 다시 객체로 되돌림
// 객체를 배열로 변환해서 map 적용
Object.entries(prices).map(([key, value]) => [key, value * 2])
);
doublePrices.meat; // 8
개발을 하다 보면 함수에 객체나 배열을 전달해야 할 때가 있다. 또, 가끔은 객체나 배열에 저장된 데이터 중 일부만 필요한 경우가 생긴다.
이럴 때 객체나 배열을 변수로 분해할 수 있게 해주는 특별한 문법인 구조 분해 할당
을 사용할 수 있다.
// 배열 구조 분해 할당
let arr = ["Bora", "Lee"]
let [firstName, surname] = arr;
firstName; // Bora
surname; // Lee
// 문자열을 배열로 변환하고, 구조 분해 할당
let [firstName, surname] = "Bora Lee".split(" ");
쉼표를 사용하여 필요하지 않은 배열의 요소를 무시할 수 있다.
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
title; // Consul
배열, 문자열 뿐만 아니라 할당 연산자 우측에는 모든 이터러블이 올 수 있다.
let [a, b, c] = "abc";
let [one, two, three] = new Set([1, 2, 3]);
또, 할당 연산자 좌측에는 변수뿐만 아니라, 객체의 프로퍼티가 같이 할당할 수 있는 어떤 것이든 올 수 있다.
let user = {};
[user.name, user.surname] = "Bora Lee".split(" ");
user.name; // Bora
구조 분해 할당을 이용해서 변수 교환 트릭도 할 수 있다.
let guest = "Jane";
let admin = "Pete";
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane
...
로 나머지 요소를 가져올 수 있다.
rest는 나머지 배열 요소들이 저장된 새로운 배열이 된다.
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
name1; // Julius
name2; // Caesar
// rest는 배열이다.
rest[0]; // Consul
rest[1]; // of the Roman Republic
할당할 값이 없을 때 기본으로 할당해 줄 기본값을 설정할 수 있다. 기본값에는 복잡한 표현식이나 함수 호출도 올 수 있다.
let [firstName, surname] = [];
firstName; // undefined
surname; // undefined
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
name; // Julius
surname; // Anonymous
// 좌측의 할당 받는 부분의 이름은 객체의 프로퍼티와 같아야 한다.
let {var1, var2} = {var1: ..., var2: ...};
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
title; // Menu
width; // 100
height; // 200
객체의 구조 분해 할당은 순서가 중요하지 않다.
let {height, width, title} = { title: "Menu", height: 200, width: 100 }
콜론 :
을 사용해서 프로퍼티 키와 다른 이름을 가진 변수를 저장할 수도 있다.
let options = {
title: "Menu",
width: 100,
height: 200
};
let {width: w, height: h, title} = options;
title; // Menu
w; // 100
h; // 200
프로퍼티가 없는 경우에 대비해서 기본값을 설정할 수 있다. 기본값에는 표현식이나 함수 호출이 올 수도 있다.
let options = {
title: "Menu"
};
let {width = 100, height = 200, title} = options;
title; // Menu
width; // 100
height; // 200
객체에서 원하는 정보만 뽑아올 수도 있다. 배열과 달리 콤마로 구별하지 않아도 된다.
let options = {
title: "Menu",
width: 100,
height: 200
};
let { title } = options;
title; // Menu
...
패턴을 사용해서, 변수에 할당받지 않는 나머지 프로퍼티들을 객체로 받을 수 있다.
let options = {
title: "Menu",
height: 200,
width: 100
};
let {title, ...rest} = options;
// rest = { height: 200, width: 100 }
rest.height; // 200
rest.width; // 100
let
으로 새로 변수를 생성하여 구조 분해 할당을 하지 않고, 기존의 존재하는 변수에 구조 분해 할당할 수 있다. 다만, 자바스크립트에서는 표현식 안에 있지 않은 { }
를 코드 블록으로 인식하므로, 구조 분해 할당문을 괄호 ()
로 감싸서 자바스크립트가 표현식으로 해석하게 하면 된다.
let title, width, height;
({title, width, height}) = {title: "Menu", width: 200, height: 100}});
title; // Menu
객체나 배열이 다른 객체나 배열을 중첩으로 포함하는 경우, 좀 더 복잡한 패턴을 사용해서 중첩 배열이나 객체의 정보를 추출할 수 있다.
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
let {
size: {
width,
height
},
items: [item1, item2],
title = "Menu"
} = options;
title; // Menu
width; // 100
height; // 200
item1; // Cake
item2; // Donut
지저분한 여러 개의 매개변수를 하나의 객체로 모아 함수에 전달하고, 함수가 인수로 전달받은 객체를 구조 분해하여 변수에 할당하고 원하는 작업을 하여 효율적인 코드를 작성할 수 있다.
// 구조 분해 활용 전
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
// 매개 변수의 순서도 틀리지 않고, 매개변수도 모두 전달해주어야 에러가 발생하지 않는다.
// 기본값이 있어 불필요한 인수를 전달 해준다.
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]);
// 구조 분해 활용
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
alert( `${title} ${width} ${height}` ); // My Menu 200 100
alert( items ); // Item1, Item2
}
showMenu(options);
참고로, 구조 분해 변수에 콜론 :
으로 변수 이름을 따로 지정하지 않는 경우에는 구조 분해 할당 문법에 따라 프로퍼티 이름에 맞춰야 한다.
function({ propertyName: varName }) {
// ...
}
JSON(JavaScript Object Notation)은 값이나 객체를 나타내주는 범용 포맷이다. 본래 자바스크립트에서 사용할 목적으로 만들어졌다. 그런데 라이브러리를 사용하면 자바스크립트가 아닌 언어에서도 JSON을 다룰 수 있어서, JSON을 데이터 교환 목적으로 사용하는 경우가 많다.
네트워크를 통해 객체를 어딘가에 보내거나 받을 때, 로깅 목적으로 객체를 출력해야 한다면 객체를 문자열로 전환해야 한다.
또, 전환된 문자열에는 객체 프로퍼티가 모두 포함되어 있어야한다.
이 때, 사용할 수 있는 내장 함수가 있다.
JSON.stringify(obj)
: 객체를 JSON으로 바꿔준다.JSON.parse(json)
: JSON을 객체로 바꿔준다.
// JSON.stringify 예시
let student = {
name: "John",
age: 30,
isAdmin: false,
course: ["html", "css", "js"],
wife: null
};
let json = JSON.stringify(student);
alert(json);
/*
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"wife": null
}
*/
객체는 이렇게 문자열로 변환된 후에 네트워크를 통해 전송하거나 저장소에 저장할 수 있다.
JSON으로 인코딩된 문자열은 일반 객체와 다른 특징을 보인다.
- 문자열은 큰따옴표로만 감싸야한다. 작은따옴표나 백틱은 사용할 수 없다.
- 객체 프로퍼티 이름은 큰따옴표로 감싸야 한다.
JSON.stringify()
는 객체뿐만 아니라 원시값에도 적용할 수 있다. 적용할 수 있는 자료형은 아래와 같다.
객체
배열
문자형
숫자형
불린형
null
JSON.stringify(1); // '1'
JSON.stringify('test'); // '"test"'
JSON.stringify(true); // 'true'
JSON.stringify([1, 2, 3]); // '[1,2,3]'
JSON은 데이터 교환을 목적으로 만들어진, 언어에 종속되지 않는 포맷이다. 따라서, 자바스크립트만의 객체 프로퍼티는 JSON.stringify()
가 처리할 수 없다. JSON.stringify()
호출 시 무시되는 프로퍼티는 아래와 같다.
함수 프로퍼티(메서드)
심볼형 프로퍼티(키가 심볼인 프로퍼티)
값이 undefined인 프로퍼티
let user = {
sayHi() { // 무시
alert("Hello");
},
[Symbol("id")]: 123, // 무시
something: undefined // 무시
};
alert( JSON.stringify(user) ); // {} (빈 객체가 출력됨, 자료형은 string임)
JSON.stringify()
는 중첩 객체도 알아서 문자열로 바꿔준다.
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( JSON.stringify(meetup) );
/* 객체 전체가 문자열로 변환되었습니다.
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
다만, JSON.stringify()
는 순환 참조가 있으면 원하는 대로 객체를 문자열로 바꿀 수 없다.
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
meetup.place = room; // meetup은 room을 참조합니다.
room.occupiedBy = meetup; // room은 meetup을 참조합니다.
JSON.stringify(meetup); // Error: Converting circular structure to JSON
JSON.stringify()
의 전체 문법은 아래와 같다.
let json = JSON.stringify(value, [replacer, space))
// value : 인코딩하려는 값
// replacer : 인코딩 하길 원하는 프로퍼티가 담긴 배열 또는 매핑 함수 function(key, value)
// space : 서식 변경 목적으로 사용할 공백 문자 수
JSON으로 변환하길 원하는 프로퍼티가 담긴 배열을 두 번째 인수로 넘겨주면 해당 프로퍼티들만 인코딩할 수 있다. 순환 참조를 발생시키는 프로퍼티 room.occupiedBy
만 제외하고 모든 프로퍼티를 배열에 넣어보자.
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup references room
};
room.occupiedBy = meetup; // room references meetup
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
*/
배열이 길게 느껴지면 배열 대신 함수를 전달할 수 있다. replacer
에 전달되는 함수는 프로퍼티 키-값 쌍 전체를 대상으로 호출되는데, 반드시 기존 프로퍼티 값을 대신하여 사용할 값을 return
문으로 반환해야 한다. 특정 프로퍼티를 직렬화에서 누락시키려면 반환값을 undefined로 만들면 된다.
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'occupiedBy') ? undefined : value;
}));
/* replacer 함수에서 처리하는 키:값 쌍 목록
: [object Object]
title: Conference
participants: [object Object],[object Object]
0: [object Object]
name: John
1: [object Object]
name: Alice
place: [object Object]
number: 23
*/
replacer
함수가 중첩 객체와 배열의 요소까지 포함한 모든 키-값 쌍을 처리하고 있다. replacer
함수는 재귀적으로 키-값 쌍을 처리하는데, 함수 내에서 this
는 현재 처리하고 있는 프로퍼티가 위치한 객체를 가리킨다.
첫 줄에 예상치 못한 문자열:[object Object]
가 뜨는 것을 볼 수 있는데, 이는 함수가 최초로 호출될 때 {"": meetup}
형태의 래퍼 객체가 만들어지기 때문이다. replacer
함수가 가장 처음으로 처리해야하는 키-값 쌍에서 키는 빈 문자열, 값은 변환하고자 하는 객체 전체가 되는 것이다.
이렇게 replacer
함수를 사용하면 중첩 객체 등을 포함한 객체 전체에서 원하는 프로퍼티만 선택해 직렬화 할 수 있다.
JSON.stringify(value, [replacer, space])
의 세 번째 인수 space
는 가독성을 높이기 위해 중간에 삽입해 줄 공백 문자 수를 나타낸다.
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
alert(JSON.stringify(user, null, 2));
/* 공백 문자 두 개를 사용하여 들여쓰기함:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
/* JSON.stringify(user, null, 4)라면 아래와 같이 좀 더 들여써짐
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
객체에 toJSON
이라는 메서드가 구현되어 있으면, 객체를 해당 메서드에서 구현한 코드대로 JSON으로 바꿀 수 있다.
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
JSON.stringify(room); // '23'
JSON.parse()
메서드를 사용하면 JSON으로 인코딩된 객체를 다시 객체로 디코딩할 수 있다.
let value = JSON.parse(str, [reviver]);
// str : JSON 형식의 문자열
// reviver : 모든 키-값 쌍을 대상으로 호출되는 함수로 값을 변경시킬 수 있다. function(key,value)
문자열로 변환된 배열을 다시 배열로 디코딩해보자.
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
numbers; // [0, 1, 2, 3]
아래와 같이 중첩 객체에도 사용할 수 있다.
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
let user = JSON.parse(userData);
user.friends[1]; // 1
참고로, JSON은 주석을 지원하지 않는다.
서버로부터 문자열로 변환된 객체를 전송받았다고 가정하자. 이제 이 문자열을 역직렬화해서 자바스크립트 객체를 만들어보자.
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str);
alert( meetup.date.getDate() ); // 에러
meetup.date
의 값은 Date
객체가 아닌, 문자열이기 때문에 발생한 에러이다. 그렇다면 문자열을 Date
로 전환해줘야 한다는 것을 어떻게 JSON.parse()
메서드에 알릴 수 있을까?
이 때 사용하는 것이 두 번째 인수 reviver
이다.
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( meetup.date.getDate() ); // 2017-11-30T12:00:00.000Z
Date
는 날짜를 저장할 수 있고, 날짜와 관련된 메서드를 제공해주는 내장 객체이다.
Date
객체를 활용하면 생성 및 수정 시간을 저장하거나 시간을 측정할 수 있고, 현재 날짜를 출력하는 용도 등으로 활용할 수 있습니다.
new Date()
를 인수없이 호출하면 현재 날짜와 시간이 저장된 Date
객체가 반환된다.
1970년 1월 1일 0시 0분 0초를 기준으로 흘러간 밀리초를 나타내는 정수는 타임스탬프(timestamp)
라고 부른다. new Date(timestamp)
로 호출하면 UTC 기준으로 1970년 1월 1일 0시 0분 0초부터 timestamp
후의 시점이 저장된 Date
객체가 반환된다.
let now = new Date();
alert( now ); // 현재 날짜 및 시간이 출력됨
// Mon Oct 24 2022 15:59:40 GMT+0900 (한국 표준시)
let Jan01_1970 = new Date(0);
alert( Jan01_1970 ); // Thu Jan 01 1970 09:00:00 GMT+0900 (한국 표준시)
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 ); // Fri Jan 02 1970 09:00:00 GMT+0900 (한국 표준시)
let Dec31_1969 = new Date(-24 * 3600 * 1000);
alert( Dec31_1969 ); // Wed Dec 31 1969 09:00:00 GMT+0900 (한국 표준시)
new Date(datestring)
는 인수로 받은 문자열을 자동으로 구문 분석하여 날짜를 저장한다.
let date = new Date("2017-01-26");
alert(date);
// 인수로 시간은 지정하지 않았기 때문에 GMT 자정이라고 가정하고
// 코드가 실행되는 시간대(timezone)에 따라 출력 문자열이 바뀜.
// 따라서 얼럿 창엔
// Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
// 혹은
// Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)등이 출력됨
new Date(year, month, date, hours, seconds, ms)
는 주어진 인수를 조합해서 만들 수 있는 날짜가 저장된 객체를 반환한다. 해당 호출 방식은 다음의 규칙을 따른다.
- 첫 번째와 두 번째 인수는 필수값이다.
year
는 반드시 네 자리 숫자여야 한다.2013
은 괜찮지만98
은 안된다.month
는 0(1월)부터 11(12월) 사이의 숫자여야 한다.date
는 일을 나타내는데, 값이 없는 경우에는 1일로 처리된다.hours / minutes / seconds / ms
에 값이 없는 경우에는 0으로 처리된다.
new Date(2011, 0, 1, 0, 0, 0, 0); // Sat Jan 01 2011 00:00:00 GMT+0900 (한국 표준시)
Date
객체의 메서드를 사용해서 연, 월, 일 등의 값을 얻을 수 있다.
getFullYear()
: 네 자릿수 연도를 반환한다.getMonth()
: 0 이상 11 이하의 숫자로 월을 반환한다.getDate()
: 1 이상 31 이하의 일을 반환한다.getHours()
,getMinutes()
,getSeconds()
,getMilliseconds()
: 시, 분, 초, 밀리초를 반환한다.getDay()
: 일요일을 나타내는 0부터 토요일을 나타내는 6 까지의 숫자 중 하나를 반환한다.getTime()
: 주어진 일시와 1970년 1월 1일 00시 00분 00초 사이의 간격인 타임스탬프를 밀리세컨드 단위로 반환한다.getTimezoneOffset()
: 현지 시간과 표준 시간의 차이를 분 단위로 반환한다.
위 메서드는 모두 현지 시간 기준으로 값을 반환한다. get
다음에 UTC
를 붙여주면 표준시(UTC+0) 기준의 날짜 구성 요소를 반환해주는 메서드를 만들 수 있다. 단, getTime()
과 getTimezoneOffset()
메서드는 표준시 기준의 메서드가 없다.
let date = new Date();
// 현지 시간 기준
alert( date.getHours() );
// 표준시간대 기준
alert( date.getUTCHours() );
alert( new Date().getTimezoneOffset() );
아래 메서드를 사용해서 날짜 구성요소를 설정할 수 있다.
setFullYear(year, [month], [date])
setMonth(month, [date])
setDate(date)
setHours(hour, [min], [sec], [ms])
setMinutes(min, [sec], [ms])
setSeconds(sec, [ms])
setMillisecons(ms)
setTime(milliseconds)
: 1970년 1월 1일 00:00:00 UTC부터milliseconds
이후를 나타내는 날짜를 설정한다.
setTime()
을 제외한 모든 메서드는 set
다음에 UTC
를 붙여서 표준시에 따라 날짜 구성 요소를 설정해주는 메서드가 있다.
let today = new Date();
// 날짜는 변경되지 않고 시만 0 으로 변경된다.
today.setHours(0);
alert(today); // Mon Oct 24 2022 00:48:37 GMT+0900 (한국 표준시)
// 날짜는 변경되지 않고, 시, 분, 초가 모두 변경된다.
today.setHours(0, 0, 0, 0);
alert(today); // Mon Oct 24 2022 00:00:00 GMT+0900 (한국 표준시)
today.setUTCMinutes(42);
alert(today); // Mon Oct 24 2022 00:42:00 GMT+0900 (한국 표준시)
Date
객체에 범위를 벗어나는 값을 설정하려고 하면 자동 고침 기능이 활성화되면서 값이 자동으로 수정된다.
입력받은 날짜 구성 요소가 범위를 벗어나면 초과분은 자동으로 다른 날짜 구성 요소에 배분된다.
let date = new Date(2013, 0, 32); // 1월 32일은 없다.
alert(date); // 2월 1일로 자동 고침
let date = new Date(2016, 2, 28);
date.setDate(date.getDate() + 2);
alert ( date ); // 윤년이면 2016년 3월 1일, 아니면 2016년 3월 2일로 자동 고침
Date
객체를 숫자형으로 변경하면, 타임스탬프가 된다.
let date = new Date();
alert(+date); // == date.getTime()을 호출하는 것과 같다.
이를 응용하면 날짜에 마이너스 연산자를 적용해서 밀리초 기준 시차를 구할 수 있다.
let start = new Date();
for (let i = 0; i < 100000; i++) {
// do something
}
let end = new Date();
alert( `반복문의 연산 시간은 ${end - start} 밀리초 입니다.` );
현재 타임스탬프를 반환하는 Date.now()
메서드를 사용해서, Date
객체를 만들지 않고도 시차를 측정할 수 있다.
Date.now()
메서드는 new Date().getTime()
과 의미론적으로 동일하지만, Date
객체를 만들지 않는다는 점이 다르다. 따라서 new Date().getTime()
을 사용하는 것보다 빠르고, 가비지 컬렉터의 일을 덜어준다는 장점이 있다.
let start = Date.now();
for (let i = 0; i < 100000; i++) {
// do something
}
let end = Date.now();
alert( `반복문의 연산 시간은 ${end - start} 밀리초 입니다.` );
Date.parse(str)
메서드를 사용하면 문자열에서 날짜를 읽어올 수 있다.
단, 문자열의 형식은 YYYY-MM-DDTHH:mm:ss.sssZ
여야 한다.
YYYY-MM-DD
: 연-월-일T
: 구분 기호HH:mm:ss.sss
: 시:분:초.밀리초Z
: 옵션으로,+-hh:mm
형식의 시간대를 나타낸다.Z
가 한 글자인 경우에는 UTC+0을 나타낸다.
YYYY-MM-DD
, YYYY-MM
, YYYY
와 같이 더 짦은 문자열 형식도 가능하다.
위 조건을 만족하는 문자열을 대상으로 Date.parse(str)
를 호출하면 문자열과 대응하는 날짜의 타임스탬프가 반환된다. 문자열의 형식이 조건에 맞지 않은 경우에는 NaN
이 반환된다.
let ms = Date.parse("2012-01-26T13:51:50.417-07:00");
alert(ms); // 1327611110417
// Date.parse(str) 메서드를 사용해서 반환받은 타임스탬프로 새로운 Date 객체 생성
let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );
alert(date); // Fri Jan 27 2012 05:51:50 GMT+0900 (한국 표준시)