-
자료구조와 자료형javascript 2023. 5. 7. 04:37
배열
배열은 순서가 있는 컬렉션을 저장할 때 쓰는 자료구조이다.
let fruits = new Array(); let fruits = []; let fruits = ["사과", "오렌지", "자두"];
위와 같은 방법으로 빈 배열을 선언해주거나 혹은 초기 요소를 넣어서 초기화해줄 수 있다.
만일 배열을 수정하고 싶다면
fruits[2] = '배';
위와 같이 인덱스로 접근해서 수정하면 된다.
자바스크립트에서는 배열 요소 자료형에 제약이 없기 때문에 아래와 같이 여러가지 자료형을 섞어서 만들어도 된다.
let arr = [ '사과', { name: '이보라' }, true, function() { alert('안녕하세요.'); } ];
큐와 스택
자바스크립트에서 배열은 큐와 스택으로도 사용할 수 있다.
큐
큐란 먼저 들어온 것이 먼저 나가는 구조의 자료구조(선입선출(First-In-First-Out, FIFO) 자료구조)이다.
맨 끝에 요소를 추가하고 싶다면push
, 제일 앞 요소를 꺼내 제거하고 싶다면shift
를 이용한다.let fruits = ["사과", "오렌지", "배"]; fruits.shift() // 배열에서 "사과"를 제거합니다. alert( fruits ); // 오렌지,배
스택
스택이란 나중에 들어온 것이 먼저 나가는 구조의 자료구조(후입선출(Last-In-First-Out, LIFO))이다.
맨 끝에 요소를 추가하고 싶다면push
, 맨 끝 요소를 추출하고 싶다면pop
을 이용한다.let fruits = ["사과", "오렌지", "배"]; fruits.pop() // 배열에서 "배"를 제거합니다. alert( fruits ); // 사과,오렌지
공통적으로 맨 끝에 요소를 추가하는 방법이
push
인데, 만일 배열 맨 앞에 요소를 추가하고 싶다면,unshift
를 이용하면 된다.let fruits = ["사과"]; fruits.push("오렌지", "배"); // ["사과", "오렌지", "배"] fruits.unshift("파인애플", "레몬"); // ["파인애플", "레몬", "사과", "오렌지", "배"]
하지만, push, pop보다 shift, unshift의 속도는 더 느리다.
배열 맨 앞의 요소를 빼주는 shift는 인덱스가 0인 요소를 제거하는 역할 뿐만 아니라, 모든 요소를 왼쪽으로 이동시켜서 1은 0으로, 2는 1으로 옮겨주어야한다. 또한 배열의 길이가 줄어들었기 때문에 length 프로퍼티 값도 갱신해주어야한다. 맨 앞에 요소를 추가해주는 unshift도 마찬가지다.
짧은 배열이라면 괜찮겠지만 배열이 길수록, 배열에 요소가 많을 수록 요소가 이동하는 데 걸리는 시간이 길어지고 메모리 관련 연산도 많아지기 때문에, shift, unshift는 속도가 느린 것이다.배열의 내부 동작 원리
배열은 특별한 종류의 객체다. 즉, 배열은 자바스크립트의 원시 자료형에 해당하지 않고 객체형에 속하기 때문에 객체처럼 동작한다.
지난 객체 게시글에서 살펴보았다시피, 객체는 그냥 복사를 한 경우 참조하는 공간을 복사하는 것이기 때문에 결국 같은 곳을 참조하게 된다는 특징을 이야기한 적이 있다.
아래의 코드에서 살펴볼 수 있듯, arr 배열과 fruits 배열 모두 같은 객체를 참조하고 있기 때문에 arr에 값을 추가해도 fruits 또한 같은 값을 갖게 된다.let fruits = ["바나나"] let arr = fruits; // 참조를 복사함(두 변수가 같은 객체를 참조) alert( arr === fruits ); // true arr.push("배"); // 참조를 이용해 배열을 수정합니다. alert( arr ); // ["바나나", "배"] alert( fruits ); // ["바나나", "배"]
반복문
배열을 순회하는 방법에는 세 가지가 있다.
let arr = ["사과", "오렌지", "배"]; // 순회방법1 for (let i = 0; i < arr.length; i++) { alert( arr[i] ); } //순회방법2 // 배열 요소를 대상으로 반복 작업을 수행합니다. for (let fruit of fruits) { alert( fruit ); } //순회방법3 for (let key in arr) { alert( arr[key] ); // 사과, 오렌지, 배 }
하지만, for in 반복문의 경우 모든 프로퍼티를 대상으로 순회하고 객체와 함께 사용 시에 최적화되어있기 때문에, 배열에 사용하면 속도가 10~100배 정도 느리다고 하니, 배열을 순회할 때에 for in은 쓰지 않는 것이 좋다고 한다.
length
let arr = [1, 2, 3, 4, 5]; alert( arr.length ) // 5
위와 같이 배열의 길이를 구하는데에 length를 사용할 수도 있지만, 배열을 자르는데에도 이용할 수 있다.
let arr = [1, 2, 3, 4, 5]; arr.length = 2; // 요소 2개만 남기고 잘린다. alert( arr ); // [1, 2] arr.length = 5; // 본래 길이로 되돌리기 시도 alert( arr[3] ); // undefined: 삭제된 기존 요소들이 복구되지 않는다! arr.length = 0; // 빈 배열
new Array()
let arr = ["사과", "배", "기타"]; let arr = new Array("사과", "배", "기타");
앞서 이야기했듯 배열은 위의 두 가지 방법으로 만들 수 있다. 대괄호를 이용하는 것이 짧고 간결해서 new Array()방식은 잘 사용되지는 않는 편이다. 뿐만 아니라, new Array()를 사용하면 실수를 유발할 수 있다.
let arr = new Array(2); // 이렇게 하면 배열 [2]가 만들어질까요? alert( arr[0] ); // undefined가 출력됩니다. 요소가 하나도 없는 배열이 만들어졌네요. alert( arr.length ); // 길이는 2입니다.
위와 같이 길이가 2인 배열이 만들어졌지만, 그 안의 요소는 undefined가 되어버린다.
toString
배열에는 toString 메소드가 자동 구현되어있어서 호출을 하면 쉼표로 구분한 문자열이 반환된다!
let arr = [1, 2, 3]; alert( arr ); // 1,2,3 alert( String(arr) === '1,2,3' ); // true alert( [] + 1 ); // "1" alert( [1] + 1 ); // "11" alert( [1,2] + 1 ); // "1,21"
배열과 메서드
splice
splice는 배열의 요소를 자유자재로 추가, 삭제, 교체하는 메소드다.
let arr1 = ["I", "study", "JavaScript"]; arr1.splice(1, 1); // 인덱스 1부터 요소 한 개를 제거 alert( arr1 ); // ["I", "JavaScript"] let arr2 = ["I", "study", "JavaScript", "right", "now"]; arr2.splice(0, 3, "Let's", "dance"); // 처음(0) 세 개(3)의 요소를 지우고, 이 자리를 다른 요소로 대체합니다. alert( arr2 ) // now ["Let's", "dance", "right", "now"]
위에서 처럼
arr.splice(index[, deleteCount, elem1, ..., elemN])
이렇게 사용한다.삭제된 요소를 변수에 담으면 배열로 반환할 수도 있다.
let arr = ["I", "study", "JavaScript", "right", "now"]; // 처음 두 개의 요소를 삭제함 let removed = arr.splice(0, 2); alert( removed ); // "I", "study" <-- 삭제된 요소로 구성된 배열
만일 deleteCount에 0을 넣어서 삭제하는 요소의 갯수가 없다면, 요소를 단순히 추가하는 방법으로 이용된다.
let arr = ["I", "study", "JavaScript"]; // 인덱스 2부터 // 0개의 요소를 삭제합니다. // 그 후, "complex"와 "language"를 추가합니다. arr.splice(2, 0, "complex", "language"); alert( arr ); // "I", "study", "complex", "language", "JavaScript"
slice
slice는 splice보다 훨씬 간단하다.
arr.slice([start], [end])
이렇게 start인덱스부터 end인덱스까지의 요소를 복사해서 새로운 배열을 반환하는 메소드가 slice이다.
let arr = ["t", "e", "s", "t"]; alert( arr.slice(1, 3) ); // e,s (인덱스가 1인 요소부터 인덱스가 3인 요소까지를 복사(인덱스가 3인 요소는 제외)) alert( arr.slice(-2) ); // s,t (인덱스가 -2인 요소부터 제일 끝 요소까지를 복사)
위에서 처럼, 만일 end값을 작성하지 않았다면 start 인덱스부터 제일 끝까지 요소를 의미하게 된다.
concat
concat은 기존 배열의 요소를 사용해 새로운 배열을 만들거나 기존 배열에 요소를 추가하고자 할 때 사용할 수 있다.
arr.concat(arg1, arg2...)
위와 같이 호출한다면 arr에 속한 모든 요소와 arg1, arg2 등에 속한 모든 요소를 한데 모은 새로운 배열이 반환된다.
let arr = [1, 2]; // arr의 요소 모두와 [3,4]의 요소 모두를 한데 모은 새로운 배열이 만들어집니다. alert( arr.concat([3, 4]) ); // 1,2,3,4 // arr의 요소 모두와 [3,4]의 요소 모두, [5,6]의 요소 모두를 모은 새로운 배열이 만들어집니다. alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 // arr의 요소 모두와 [3,4]의 요소 모두, 5와 6을 한데 모은 새로운 배열이 만들어집니다. alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
즉, concat은 기존 배열을 복사하고 추가적인 요소를 넣을 때 유용하다.
forEach로 반복작업 하기
let arr = ["Bilbo", "Gandalf", "Nazgul"]; arr.forEach(function(item, index, array) { alert(`${item} is at index ${index} in ${array}`); }); // Bilbo is at index 0 in ["Bilbo", "Gandalf", "Nazgul"] // Gandalf is at index 1 in ["Bilbo", "Gandalf", "Nazgul"] // Nazgul is at index 2 in ["Bilbo", "Gandalf", "Nazgul"]
forEach는 위와 같이 작성할 수 있다.
위의 코드는 forEach문을 돌리는 array의 index값에 해당하는 item을 보여준다.배열 탐색하기
indexOf, lastIndexOf, includes
배열 탐색 방법에는 indexOf, lastIndexOf와 includes가 있다.
arr.indexOf(item, from)는 인덱스 from부터 시작해 item(요소)을 찾는데, 요소를 발견하면 해당 요소의 인덱스를 반환하고, 발견하지 못했으면 -1을 반환하게 된다.
arr.lastIndexOf(item, from)는 indexOf와 동일한 기능이지만 검색을 끝에서부터 시작한다는 점만 다르다.
arr.includes(item, from)는 인덱스 from부터 시작해 item이 있는지를 검색하는데, 해당하는 요소를 발견하면 true를 반환한다.let arr = [1, 0, false]; alert( arr.indexOf(0) ); // 1 alert( arr.indexOf(false) ); // 2 alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true
find
특정 조건에 부합하는 객체를 배열 내에서 찾고 싶다면, 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
이렇게 해당하는 요소가 있다면 해당 요소를 반환하고 없다면 undefined를 반환한다.
filter
let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"} ]; // 앞쪽 사용자 두 명을 반환합니다. let someUsers = users.filter(item => item.id < 3); alert(someUsers); // [{id: 1, name: "John"},{id: 2, name: "Pete"}] alert(someUsers.length); // 2
filter를 이용할 수도 있다. find와 달리 filter는 조건에 부합하는 요소 전체를 배열에 담아 반환할 수 있다.
map
map은 배열 요소 전체를 대상으로 함수를 호출하고, 함수 호출 결과를 배열로 반환해주는 메소드이다.let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6
sort
sort는 배열의 요소를 정렬해주고, 이 때 배열 자체가 변경된다.let arr = [ 1, 2, 15 ]; // arr 내부가 재 정렬됩니다. arr.sort(); alert( arr ); // 1, 15, 2
이 때, 1, 2, 15가 아니라 1, 15, 2로 정렬되는 이유는 배열 요소가 문자열로 취급되었기 때문이다.
1, 2, 15와 같이 정렬하고 싶다면 아래와 같이 조건을 추가해주어야한다.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
reduce와 reduceRight
reduce와 reduceRight는 forEach, for, for ... of, map과 유사한 역할을 하는 메소드이지만, 배열을 기반으로 값 하나를 도출할 때 사용한다.let arr = [1, 2, 3, 4, 5]; let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15
reduce의 마지막 인수로 적힌 0은 sum의 초기값으로 할당되어 배열을 모두 순회하며 더해진 값이 result가 되는 것이다.
Array.isArray로 배열 여부 알아내기
alert(Array.isArray({})); // false alert(Array.isArray([])); // true
만일 배열이 맞다면 true를 아니라면 false를 반환해준다.
배열 메서드와 ‘thisArg’
thisArg는 func의 this가 되는 매개변수이다.arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg);
iterable 객체
반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체이다.
배열은 대표적인 이터러블이다.Symbol.iterator
이터러블엔 메서드 Symbol.iterator가 반드시 구현되어 있어야 한다.
let range = { from: 1, to: 5 };
만일 위의 객체를 1부터 5까지 for문을 돌리는 것이 목표라면 아래와 같이 작성해줄 수 있다.
let range = { from: 1, to: 5 }; // 1. for..of 최초 호출 시, Symbol.iterator가 호출됩니다. range[Symbol.iterator] = function() { // Symbol.iterator는 이터레이터 객체를 반환합니다. // 2. 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다. return { current: this.from, last: this.to, // 3. for..of 반복문에 의해 반복마다 next()가 호출됩니다. next() { // 4. next()는 값을 객체 {done:.., value :...}형태로 반환해야 합니다. if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 이제 의도한 대로 동작합니다! for (let num of range) { alert(num); // 1, then 2, 3, 4, 5 }
여기서 range 객체 안에 이터레이터를 넣어주어 range자체를 이터레이터로 만들면 코드가 더 간단해진다.
let range = { from: 1, to: 5, [Symbol.iterator]() { this.current = this.from; 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, then 2, 3, 4, 5 }
문자열은 이터러블입니다
앞서 언급했듯 이터러블은 반복 가능한 것을 의미한다. 배열과 문자열은 가장 광범위하게 쓰이는 내장 이터러블이다.이터러블과 유사 배열
이터러블(iterable) 은 위에서 설명한 바와 같이 메서드 Symbol.iterator가 구현된 객체라면,
유사 배열(array-like) 은 인덱스와 length 프로퍼티가 있어서 배열처럼 보이는 객체이다.Array.from
Array.from(obj[, mapFn, thisArg])을 사용하면 이터러블이나 유사 배열인 obj를 진짜 Array로 만들 수 있다.let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // World (메서드가 제대로 동작합니다.)
Array.from(arrayLike); 를 통해 이터러블인지 유사배열인지 조사한다.
또한,Array.from(obj[, mapFn, thisArg])
를 통해 매핑함수를 선택적으로 넘겨줄 수도 있다.선택 인수 mapFn와 thisArg는 각 요소에 함수를 적용할 수 있게 해준다.맵과 셋
맵(Map)은 키가 있는 데이터를 저장한다는 점에서 객체와 유사하지만 맵은 키에 다양한 자료형을 허용한다는 점에서 차이가 있다.
let map = new Map(); //맵을 만든다. // map.set(key, value) key를 이용해 value를 저장한다. map.set('1', 'str1'); // 문자형 키 map.set(1, 'num1'); // 숫자형 키 map.set(true, 'bool1'); // 불린형 키 // 객체는 키를 문자형으로 변환한다는 걸 기억하고 계신가요? // 맵은 키의 타입을 변환시키지 않고 그대로 유지합니다. 따라서 아래의 코드는 출력되는 값이 다릅니다. // map.get(key) – key에 해당하는 값을 반환합니다. key가 존재하지 않으면 undefined를 반환한다. alert( map.get(1) ); // 'num1' alert( map.get('1') ); // 'str1' //요소의 개수를 반환한다. alert( map.size ); // 3
Object.entries: 객체를 맵으로 바꾸기
각 요소가 키-값 쌍인 배열이나 이터러블 객체를 초기화 용도로 맵에 전달해 새로운 맵을 만들 수 있다.// 각 요소가 [키, 값] 쌍인 배열 let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); alert( map.get('1') ); // str1
위의 예시는 배열을 새로운 맵으로 만든 경우이다.
만일 객체를 가지고 맵을 만들고 싶다면, new Map을 해줄 때 먼저 Object.entries(obj)를 해주어야 객체의 키-값 쌍을 요소([key, value])로 가지는 배열로 새로운 맵을 만들 수 있다.Object.fromEntries: 맵을 객체로 바꾸기
let prices = Object.fromEntries([ ['banana', 1], ['orange', 2], ['meat', 4] ]); // now prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2
위와 같이 Object.fromEntries를 이용하면 맵을 객체로 바꿀 수 있다.
set
셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이다.
//셋을 만든다. 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 순으로 출력됩니다. }
Object.keys, values, entries
Object.keys(obj) – 객체의 키만 담은 배열을 반환합니다. Object.values(obj) – 객체의 값만 담은 배열을 반환합니다. Object.entries(obj) – [키, 값] 쌍을 담은 배열을 반환합니다.
구조 분해 할당
구조 분해 할당은 객체나 배열을 변수로 '분해’할 수 있게 해주는 특별한 문법이다.
// 이름과 성을 요소로 가진 배열 let arr = ["Bora", "Lee"] // 구조 분해 할당을 이용해 // firstName엔 arr[0]을 // surname엔 arr[1]을 할당하였습니다. let [firstName, surname] = arr; alert(firstName); // Bora alert(surname); // Lee
이렇게 arr을 firstName, surname으로 분해할 수 있다.
'…'로 나머지 요소 가져오기
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
...는 배열 앞쪽에 위치한 값 몇 개만 필요하고 그 이후 이어지는 나머지 값들은 한데 모아서 저장하고 싶을 때, 나머지 요소를 가져올 수 있게 하는 문법이다.
객체 분해하기
구조 분해 할당으로 객체도 분해할 수 있다.let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200
다음과 같이 options 객체를 title, width, height로 분리하였다.
'javascript' 카테고리의 다른 글
함수 심화와 this (0) 2023.06.24 5월 데보션 테크 세미나 클라우드 비용 최적화 후기 (1) 2023.05.28 자바스크립트 함수 심화 (1) 2023.05.13 객체 (0) 2023.04.22 자바스크립트 기본 (0) 2023.04.09