본문 바로가기

JavaScript

20210711 JavaSciprt DeepDive 09 : 배열, JS배열, 배열 생성, CRUD, 배열 메서드, 스택(stack) 구현, 큐(queue) 구현

JavaScript Deep Dive 09



용어 및 중요사항 정리


배열 메서드 내용 return
new Array() 배열 생성 (생성자 함수) Instance
Array.of(itemValue) 배열 생성 전달된 요소로 구성된 배열
Array.from((유사배열, 이터러블 객체), Callback) 배열 변환 및 가공 객체를 배열로 만들고 callback 반환값으로 구성된 배열을 반환
Array.isArray(인수) 전달된 인수가 배열인지 아닌지 확인 true, false
Array.prototype.indexOf(searchValue, startSearchIndex) 요소검색 첫번째로 검색된 요소의 인덱스
Array.prototype.includes(searchValue, searchStartIndex) 배열 내에 특정 요소가 포함되어 있는지 확인 true, false
Array.prototype.push(인수) 원본배열의 마지막 요소로 추가 변경된 length 프로퍼티 값을 반환
Array.prototype.pop() 원본배열의 마지막 요소 제거 제거한 요소 반환
Array.prototype.unshift(인수) 원본배열의 선두에 요소 추가 변경된 length 프로퍼티 값을 반환
Array.prototype.shift() 원본배열에서 첫번째 요소 제거 제거한 요소 반환, 빈 배열시 undefined 반환
Array.prototype.concat() 마지막 요소로 추가한 새로운 배열 반환 새로운 배열 반환
Array.prototype.splice(start, deleteCount, items) 원본배열 추가, 제거, 수정 제거한 요소가 배열로 반환됨
Array.prototype.slice(startIndex, endIndex) 원본배열을 직접 변경하지 않고 추출, 얕은 복사 인수로 전달된 범위의 요소들을 복사하여 배열로 반환
Array.prototype.reserve() 원본 배열의 순서 반대로 변경 변경된 원본 배열
Array.prototype.fill(fillValue, startIndex, endIndex) 인수로 전달받은 값을 배열의 특정 범위를 요소로 채움 변경된 원본 배열
Array.prototype.flat(flatDepth) 중첩 배열 평탄화 평탄화된 배열 반환



배열


  • 배열이라는 타입은 존재하지 않음 -> 배열은 객체 타입임
  • 요소(Element) : 배열이 가지고 있는 값
    • JS에서는 모든 값은 배열의 요소가 될 수 있음 (원시값, 객체, 함수, 배열 ...)
    • 요소 접근시 대괄호 표기법 [index] 사용
  • 인덱스(Index) : 배열에서 자신의 위치를 나타내는 0이상의 정수
  • length 프로퍼티 : 배열의 길이를 나타내는 프로퍼티 -> for문을 통해 순차적으로 요소 접근 가능
  • 생성 방식 : 배열 리터럴, Array 생성자 함수, Array.of 메서드, Array.from 메서드
  • Array.prototype : 배열의 프로토 타입 객체, 빌트인 메서드 제공
  • 일반 객체 vs 배열
구분 객체 배열
구조 프로퍼티 키와 프로퍼티 값 인덱스와 요소
값의 참조 프로퍼티 키 인덱스
값의 순서 X O
length 프로퍼티 X O



자료구조에서 말하는 일반적인 배열


  • 밀집배열 : 동일한 크기의 메모리 공간 하나의 데이터 타입으로 통일되어 서로 연속적으로 인접해 있는 배열

  • 배열의 장점 : 순차적(or 역순)으로 요소에 접근 가능 (반복문을 사용하기 적합한 자료구조)

    • 임의 접근 : 인덱스를 활용해 한번의 연산으로 임의의 요소에 접근 가능 -> 고속 동작
  • 배열의 단점 :

    • 선형 검색 : 정렬되지 않은 배열에서 검색하는 경우 모든 요소를 처음부터 발견할 때 까지 연산 해야함 시간 복잡도 O(n)
    • 요소의 이동 의무 : 배열에 요소를 삽입, 삭제하는 경우 연속을 유지하기 위해서 요소를 이동시켜야 함



자바스크립트의 배열


  • 희소 배열 : 각각의 메모리 공간 동일 크기 X, 연속 X, 중간 중간에 빌수도 있음
    • 성능적으로 좋지 않음

  • JS 배열 :
    • 희소 배열을 지원 함
    • 일반적인 배열의 동작을 흉내낸 특수한 객체임
    • 인덱스를 나타내는 문자열 프로퍼티, length 프로퍼티를 갖음
    • 해시 테이블로 구현된 객체

  • JS 배열의 요소 : 프로퍼티 값 , js에서 사용가능한 모든 값은 객체의 프로퍼티 값이 될수 있음 -> 모든 타입의 값은 배열의 요소가 가능
    • 배열의 요소는 최대 2^32 - 1개까지 가질수 있음
/*
프로퍼티 키(인덱스) - 프로퍼티 값(데이터)
프로퍼티 키(length) - 프로퍼티 값(프로퍼티 개수)
{
  '0' : {value: 1, writable: true, enumerable: true, configurable: true}
  '1' : {value: 2, writable: true, enumerable: true, configurable: true}
  '2' : {value: 3, writable: true, enumerable: true, configurable: true}
  length : {value: 3, writable: true, enumerable: false, configurable: false}
}
 */

const arr = [
  "string",
  10,
  true,
  null,
  undefined,
  NaN,
  Infinity,
  [],
  {},
  function () {},
];

  • JS 배열의 장점/ 단점 : 검색, 삽입, 삭제의 경우에는 빠름/ 요소 접근(인덱싱)은 조금 느림 (그래도, 일반 객체 보단 2배 빠름)

  • length 프로퍼티 값 : 요소의 개수를 나타내며, 0이상 ~ 2^32-1 미만의 양의 정수 까지 값을 나타냄 (배열의 요소 개수 제한 때문에)
    • 가장 큰 인덱스 + 1
    • 배열에 요소 추가, 삭제시 length 프로퍼티 값 자동 갱신
    • length 프로퍼티 값에 명시적으로 값을 할당하는 경우,
      • 기존 프로퍼티 길이 보다 작은 숫자 할당 -> 해당 길이로 변환되어 나옴(넘치던 것은 삭제)
      • 기존 프로퍼티 길이 보다 큰 숫자 할당 -> length 프로퍼티 값은 변경, 실제 배열은 변화없음
    • 비어있는 값은 empty로 표시 됨
    • 희소배열 length와 배열 요소의 개수가 일치하지 않음
      • 언제나, 희소배열 length > 배열의 요소 개수
  • JS 엔진은 타입이 일치하는 요소 배열을 생성시 일반적인 배열처럼 연속 공간 확보함
    • 배열에는 같은 타입의 요소를 연속적으로 위치 시키자
const arr = [1, , 3, , 5];
console.log(arr); // [1, empty, 3, empty, 5]
console.log(Object.getOwnPropertyDescriptors(arr));
/* 
0: {value: 1, writable: true, enumerable: true, configurable: true}
2: {value: 3, writable: true, enumerable: true, configurable: true}
4: {value: 5, writable: true, enumerable: true, configurable: true}
length: {value: 5, writable: true, enumerable: false, configurable: false} 
*/



배열 생성


  • 배열 리터럴 : 프로퍼티 키 없이 값만 표시하는 방식 ([]대괄호와 쉼표 활용)
    • 요소 생략시 희소 배열 생성

  • Array 생성자 함수 :
    • 주의) 전달된 인수의 개수에 따라 다르게 동작함
      • 전달된 인수가 없을 때 -> 빈 배열 생성 ([ ])
      • 전달된 인수가 숫자이고 1개인 경우 -> length 값을 할당하듯이 작동 함 (요소 생성X)
      • 전달된 인수가 2개 이상이거나 숫자가 아닌경우 -> 해당 인수를 요소로 갖는 배열 생성
        • 전달된 인수가 숫자가 아니면 1개여도 요소로 인식
    • new 연산자 상관없이 생성자함수, 일반함수 호출시 모두 생성자 함수로 동작함 (내부적으로 new.target을 확인함)

  • Array.of 메서드
    • 전달된 인수를 요소로 갖는 배열 생성
    • 전달된 인수가 1개이고 숫자여도 인수를 요소로 갖는 배열 생성

  • Array.from 메서드
    • Array.from(객체, 콜백함수)
    • 첫번째 인수 : 유사 배열 객체 또는 이터러블 객체를 전달받아 배열로 변환하여 반환
      • 유사 배열 객체 : 인덱스로 프로퍼티 값에 접근 가능하고, length 프로퍼티 갖는 객체 (for문으로 순회 가능)
      • length만 있는 유사배열 객체는 length 만큼 undefined를 가진 요소를 생성
      • 이터러블 객체 : Symbol.iterator 메서드로 구현한 객체로, for...of 문 순회 가능하고, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할수 있는 객체
    • 두번째 인수 : 콜백함수를 받아 첫번째 인수에 의해 생성된 배열의 요소값
    • return : 인덱스를 순차적으로 전달하면서 호출하여 callback 반환값으로 구성된 배열을 반환
// 유사 배열 객체 -> 배열
Array.from({ length: 2, 0: "a", 1: "b" }); // [a, b]
Array.from({ length: 2 }); // [undefined, undefined]
Array.from({ length: 2, 0: "a", 1: "b" }, (v, i) => i); // [0, 1]
// 이터러블 -> 배열
Array.from("Hello"); // ['H', 'e', 'l', 'l', 'o']



배열 요소의 참조, 추가, 갱신, 삭제 (CRUD)


참조


  • Array[index] : 대괄호 표기법으로 참조함
    • index는 정수로 평가되는 표현식이면 넣을 수 있음
    • 존재하지 않는 요소 접근시 undefined 반환 (객체에서 존재하지 않는 프로퍼티 키로 접근했을 때 undefined 반환과 같음)

추가와 갱신


  • 추가 : 존재하지 않는 인덱스를 사용해 값을 할당하면 요소가 추가됨
    • 현재 배열의 length 프로퍼티 값보다 큰 인덱스로 새로운 요소 추가시 희소 배열이 됨
    • 명시적으로 값을 할당하지 않으면, 요소는 생성되지 않음
    • 정수또는 정수 형태의 문자열을 인덱스로 사용하지 않으면, 요소가 아니라 프로퍼티가 생성됨 (생성된 프로퍼티는 length에 영향을 주지 않음)
  • 갱신 : 이미 요소가 존재하는 요소에 값을 재할당 하면 갱신됨
const arr = [0];
arr[1] = 1;
console.log(arr); // [0 ,1]
console.log(arr.length); // 2

arr[40] = 40;
console.log(arr); // [0, 1, empty x38 ,40]
console.log(arr.length); // 41

arr[1] = 2;
console.log(arr); // [0, 2, empty x38 ,40]

삭제


  • delete 연산자 사용 방식
    • 배열도 객체이기 때문에, delete 연산자로 프로퍼티 키로 프로퍼티를 특정하여 삭제
    • 희소배열이 되고, length 프로퍼티 값은 변하지 않음
    • 비추천

  • Array.prototype.splice 메서드 방식
    • Array.prototype.splice(startIndex, deleteCount)
    • 희소배열 안만들고 배열의 특정 요소 완전히 삭제 가능
    • length 프로퍼티는 자동으로 갱신됨

배열 메서드


  • 원본 배열 직접 변경 메서드(mutator method) : 예) push
    • 직접 변경하는 것 보다 반환 메서드를 사용하는게 좋음 (외부상태를 직접 변경하는 부수 효과를 줄이기 위해서)
  • 새로운 배열 생성 반환 메서드(accessor method) : 예) concat

  • Array.isArray(인수) :

    • Array 생성자 함수의 정적 메서드
    • 전달된 인수가 배열인지 아닌지 true, false 반환
    • 유사 배열 객체 체크 확인 가능 (false)

  • Array.prototype.indexOf(searchValue, startSearchIndex) :

    • 원본 배열에서 인수로 전달된 요소를 검색하여 첫번째로 검색된 요소의 인덱스 반환
    • 원본 배열에 인수로 전달된 요소가 존재하지 않으면 -1 반환
    • 배열에 특정요소 존재하는지 확인시 유용

  • Array.prototype.includes(searchValue)

    • 원본 배열에서 인수로 전달된 요소가 존재하는지 확인 -> true / false

  • Array.prototype.push(인수)

    • 인수로 전달 받은 모든 값을 원본 배열의 마지막 요소로 추가
    • return : 변경된 length 프로퍼티 값을 반환
    • 원본 배열 직접 변경 -> 부수 효과 O
      • 스프레드 문법 : 부수 효과 없이 마지막 요소를 추가할 수 있음
    • arr[arr.length] = value
      • 추가할 요소가 하나 뿐이라면 직접적으로 값을 할당해서 추가하는게 빠름
      • 배열의 length 프로퍼티 값을 인덱스로 하여 동적으로 값할당해서 추가 방식

  • Array.prototype.pop()
    • 원본 배열에서 마지막 요소 제거
    • return : 제거한 요소 반환
      • 빈 배열이면 undefined 반환
    • 원본 배열을 직접 변경 -> 부수 효과 O

  • Array.prototype.unshift(인수)
    • 인수로 전달받은 모든 값을 원본 배열의 선두에 요소로 추가
    • return : 변경된 length 프로퍼티 값을 반환
    • 원본 배열 직접 변경 -> 부수효과 O
      • 스프레드 문법 사용 추천

  • Array.prototype.shift()
    • 원본 배열에서 첫번째 요소를 제거
    • return : 제거한 요소 반환, 빈 배열시 undefined 반환
    • 원본 배열 직접 변경 -> 부수효과 O

  • Array.prototype.concat()
    • return : 인수로 전달된 값들(배열 또는 원시값)을 원본 배열의 마지막 요소로 추가한 새로운 배열 반환
    • 전달된 값이 배열인 경우, 배열 해체해서 요소로 추가
    • 원본 배열은 변경되지 않음
    • 스프레드 문법으로 대체 가능

  • push, unshift, pop, shift 사용시 원본 배열을 반드시 변수에 저장해 두어야 함

  • Array.prototype.splice

    • Array.prototype.splice(start, deleteCount, items)
    • 원본 배열의 중간에 요소를 추가하거나, 중간에 있는 요소를 제거하는 경우 사용
    • 원본배열 직접 변경
    • start : 원본 배열의 요소 제거 시작할 인덱스
      • start만 넣으면 start 부터 모든 요소 제거
      • start 음수 인덱스(역순 인덱스) 지원
    • deleteCount: start 부터 제거할 요소의 개수 (옵션)
      • 0으로 하면 아무런 요소도 제거하지 않고 items를 넣을 수 있음
    • items : 제거한 위치에 삽입할 요소들의 목록, 생략시 요소들을 제거하기만 함 (옵션)
    • return : 제거한 요소가 배열로 반환됨
  • 특정 요소 제거는 indexOf + splice 조합으로 가능

    • indexOf로 특정 값 인덱스를 찾고 해당 인덱스 값으로 splice를 돌려 제거하거나, 수정 등이 가능
// 단일 제거
function remove(array, item) {
  const index = array.indexOf(item);

  if (index !== -1) array.splice(index, 1);

  return array;
}

// 중복 제거
function removeAll(array, item) {
  return array.filter((v) => v !== item);
}

  • Array.prototype.slice
    • Array.prototype.slice(startIndex, endIndex)
    • return : 인수로 전달된 범위의 요소들을 복사하여 배열로 반환
      • start : 복사를 시작할 인덱스, 음수 인덱싱을 지원함
      • end : 복사를 종료할 인덱스, 해당 인덱스는 미포함
        • end 생략시 기본값 length 프로퍼티 값 (즉, 존재하는 마지막 인덱스 까지)
    • 원본배열을 직접 변경하지 않음
    • 얕은 복사를 통해 생성됨
    • 유사 배열 객체(arguments, HTMLCollection, NodeList)를 배열로 변환 가능
      • 물론, Array.from 메서드가 더 간단하게 변경 가능
    • 주의) slice(직접 변경X) vs splice(직접 변경O)

  • Array.prototype.join
    • Array.prototype.join(구분자)
    • 원본 배열의 모든 요소를 문자열로 변환
    • return : 인수로 전달받은 구분자로 연결한 문자열을 반환
    • 구분자 생략시 기본 구분자는 콤마(,)

  • Array.prototype.reserve
    • 원본 배열의 순서를 반대로 뒤집음
    • 원본 배열이 변경됨
    • return : 변경된 원본 배열 (원본과 연결되어 있음)

  • Array.prototype.fill
    • Array.prototype.fill(fillValue, startIndex, endIndex)
    • 인수로 전달받은 값을 배열의 특정 범위를 요소로 채움
      • fillvalue : 배열에 요소를 채울 특정 값
        • fillvalue만 있으면 모든 요소를 변경함
      • startIndex : fillvalue로 채우기 시작할 index
      • endIndex : fillvalue로 채우기를 멈출 미포함 index
        • endIndex 생략시 끝까지
    • return : 변경된 원본 배열 (원본과 연결되어 있음)
    • 원본 배열이 변경됨
    • empty도 채움
    • 모든 요소를 하나의 값만으로 채울 수만 있음
      • Array.from을 사용하면, callback함수로 특정 값(여러 값)을 만들면서 인수로 채울수 있음

  • Array.prototype.includes
    • Array.prototype.includes(searchValue, searchStartIndex)
    • 배열 내에 특정 요소가 포함되어 있는지 확인
      • searchValue : 검색할 값
      • searchStartIndex : 검색 시작할 인덱스
        • 생략시, 기본값 0
        • 음수 전달시, (length + index) 검색 시작(즉, 음수 인덱싱 지원)
    • return : true or false
    • indexOf를 사용해서도 요소가 포함되어 있는지 확인 가능함(-1을 반환하는지 확인)
      • NaN은 판별이 불가함

  • Array.prototype.flat
    • Array.prototype.flat(flatDepth)
    • 인수로 전달한 깊이만큼 재귀적으로 배열을 평탄화 함(중첩 배열 평탄화)
      • flatDepth : 평탄화 할 깊이 또는 회수, 생략시 기본값 1 (안에 첫번째 중첩배열이 1)
        • Infinity를 전달하면, 중첩 배열 모두를 평탄화 시킴



스택 구현하기 (push, pop)

  • LIFO (last in first out)
    • 가장 나중에 넣은 데이터를 먼저 꺼내는 후입선출

  • 생성자 함수 방식
    • array가 인스턴스의 프로퍼티라서 인스턴스에서 접근 가능
const Stack = (function () {
  function Stack(array = []) {
    if (!Array.isArray(array)) {
      throw new TypeError(`${array} is not an Array.`);
    }
    // 인스턴스의 array 프로퍼티 = 생성자 함수에 들어온 array
    this.array = array;
  }
  Stack.prototype = {
    constructor: Stack,

    push(value) {
      return this.array.push(value);
    },

    pop() {
      return this.array.pop();
    },

    // instance에 계속 연결되게 하기 싫어서
    entries() {
      return [...this.array];
    },
  };
  // 설정이 끝난 생성자 함수 반환
  return Stack;
})();

const stack = new Stack([1, 2]);
console.log(stack.entries()); // [1, 2]

console.log(stack.push(5)); // 3 <- length 값 반환
console.log(stack.entries()); // [1, 2, 5]

console.log(stack.pop()); // 5 <- 제거한 값 반환
console.log(stack.entries()); // [1, 2]

  • 클래스 방식으로 구현
    • #array로 private로 설정 했기 때문에 인스턴스에서 참조, 접근하지 못함
class Stack {
  #array; // private class method

  constructor(array = []) {
    if (!Array.isArray(array)) {
      throw new TypeError(`${array} is not an Array.`);
    }
    this.#array = array;
  }

  push(value) {
    return this.#array.push(value);
  }

  pop() {
    return this.#array.pop();
  }

  entries() {
    return [...this.#array];
  }
}



큐 구현하기 (push, shift)


  • FIFO (frist in first out)
    • 가장 먼저 넣은 데이터를 먼저 꺼내는 선입 선출

  • 생성자 함수 방식
const Queue = (function () {
  function Queue(array = []) {
    if (!Array.isArray(array)) {
      throw new TypeError(`${array} is not an Array.`);
    }
    this.array = array;
  }

  Queue.prototype = {
    constructor: Queue,

    enqueue(value) {
      return this.array.push(value);
    },

    dequeue() {
      return this.array.shift();
    },

    entries() {
      return [...this.array];
    },
  };

  return Queue;
})();

const queue = new Queue([1, 2]);
console.log(queue.entries()); // [1, 2]

console.log(queue.enqueue(5)); // 3 <- length 값 반환
console.log(queue.entries()); // [1, 2, 5]

console.log(queue.dequeue()); // 1 <- 제거한 값 반환
console.log(queue.entries()); // [2, 5]

  • 클래스 방식
class Queue {
  #array; // private

  constructor(array) {
    if (!Array.isArray(array)) {
      throw TypeError(`${array} is not an Array`);
    }
    this.#array = array;
  }

  enqueue(value) {
    return this.#array.push(value);
  }

  dequeue() {
    return this.#array.shift();
  }

  entries() {
    return [...this.#array];
  }
}