Skip to content

Latest commit

 

History

History
1414 lines (1025 loc) · 46.2 KB

27장_배열.md

File metadata and controls

1414 lines (1025 loc) · 46.2 KB

27. 배열

27.1 배열이란?

  • 배열은 여러 개의 값을 순차적으로 나열한 자료구조
const arr = ['apple', 'banana', 'orange'];
  • 배열이 가지고 있는 값 = 요소
  • 자바스크립트의 모든 값은 배열의 요소가 될 수 있음.
  • 배열의 요소는 자신의 위치를 나타내는 인덱스를 갖는다.
arr[0] // -> 'apple'
arr[1] // -> 'banana'
arr[2] // -> 'orange'
  • 배열은 요소의 개수, 즉 배열의 길이를 나타내는 length 프로퍼티를 갖는다.
arr.length // -> 3
  • 배열은 인덱스와 프로퍼티를 갖기 때문에 for 문을 통해 순차적으로 요소에 접근할 수 있다.
// 배열의 순회
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]); // 'apple' 'banana' 'orange'
}
  • 배열이라는 타입은 존재하지 않고, 객체 타입이다.
typeof arr // -> object
  • 배열의 생성방식
    • Array 생성자 함수
    • Array.of, Array.from 메서드
    • 배열 리터럴
const arr = [1, 2, 3];

arr.constructor === Array // -> true
Object.getPrototypeOf(arr) === Array.prototype // -> true
  • 배열은 객체지만, 일반 객체와는 다른 특성을 가지고 있다.
구분 객체 배열
구조 프로퍼티 키와 프로퍼티 값 인덱스와 요소
값의 참조 프로퍼티 키 인덱스
값의 순서 X O
length 프로퍼티 X O
const arr = [1, 2, 3];

// 반복문으로 자료 구조를 순서대로 순회하기 위해서는 자료 구조의 요소에 순서대로
// 접근할 수 있어야 하며 자료 구조의 길이를 알 수 있어야 한다.
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]); // 1 2 3
}

27.2 자바스크립트 배열은 배열이 아니다.

  • 자료구조에서 말하는 배열은 동일한 크기의 메모리 공간이 빈틈없이 연속적으로 나열된 자료구조

  • 즉, 배열의 요소는 하나의 데이터 타입으로 통일, 서로 연속적으로 인접해있다.

  • 이러한 배열을 밀집 배열이라 한다.

  • 밀집 배열은 인덱스를 통해 단 한번의 연산으로 임의 요소에 접근(O(1))할 수 있다.

    검색 대상 요소의 메모리 주소 = 배열의 시작 메모리 주소 + 인덱스 * 요소의 바이트 수
    
// 선형 검색을 통해 배열(array)에 특정 요소(target)가 존재하는지 확인한다.
// 배열에 특정 요소가 존재하면 특정 요소의 인덱스를 반환하고, 존재하지 않으면 -1을 반환한다.
function linearSearch(array, target) {
  const length = array.length;

  for (let i = 0; i < length; i++) {
    if (array[i] === target) return i;
  }

  return -1;
}

console.log(linearSearch([1, 2, 3, 4, 5, 6], 3)); // 2
console.log(linearSearch([1, 2, 3, 4, 5, 6], 0)); // -1
  • 하지만 자바스크립트의 배열은 요소의 각각 메모리 공안은 동일한 크기를 갖지 않아도 되고, 연속적으로 이어져 있지 않을 수도 있다.

  • 이러한 배열을 희소배열이라 부른다.

  • 즉, 자바스크립트의 배열은 일반적인 배열의 동작을 흉내 낸 특수한 객체다.

    // "16.2. 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체" 참고
    console.log(Object.getOwnPropertyDescriptors([1, 2, 3]));
    /*
    {
      '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 () {}
];
  • 일반적인 배열과 자바스크립트의 배열의 장단점을 정리해보면

    • 일반적인 배열: 인덱스로 요소에 빠르게 접근 가능. 특정 요소를 검색하거나 삽입 삭제하는 경우 비효율적.
    • 자바스크립트 배열 : 해시 테이블로 구현된 객체, 인덱스로 요소의 접근하는 경우 성능적인 면에서 느릴 수 밖에 없는 구조적인 단점. 하지만 특정 요소를 검색하거나 요소를 삽입 또는 삭제하는 경우 일반적인 배열보다 빠른 성능을 기대
  • 자바스크립트의 배열이 객체보다 약 2배정도 빠르다.

const arr = [];

console.time('Array Performance Test');

for (let i = 0; i < 10000000; i++) {
  arr[i] = i;
}
console.timeEnd('Array Performance Test');
// 약 340ms

const obj = {};

console.time('Object Performance Test');

for (let i = 0; i < 10000000; i++) {
  obj[i] = i;
}

console.timeEnd('Object Performance Test');
// 약 600ms

27.3 length 프로퍼티와 희소 배열

  • length 프로퍼티는 요소의 개수, 즉 배열의 길이를 나타내는 0 이상의 정수의 값을 가진다.

    [].length        // -> 0
    [1, 2, 3].length // -> 3
  • length 프로퍼티의 값은 0과 232-1(4,294,967,296 - 1) 미만의 양의 정수

  • 배열의 가장 큰 인덱스는 232- 2(4,294,967,294)다.

  • length 프로퍼티의 값은 배열의 요소를 추가하거나 삭제하면 자동 갱신된다.

    const arr = [1, 2, 3];
    console.log(arr.length); // 3
    
    // 요소 추가
    arr.push(4);
    // 요소를 추가하면 length 프로퍼티의 값이 자동 갱신된다.
    console.log(arr.length); // 4
    
    // 요소 삭제
    arr.pop();
    // 요소를 삭제하면 length 프로퍼티의 값이 자동 갱신된다.
    console.log(arr.length); // 3
  • length 프로퍼티의 값을 임의로 할당할 수 있다.

  • 현재 length 프로퍼티의 값보다 작은 숫자를 할당하면 배열의 길이가 줄어든다.

    const arr = [1, 2, 3, 4, 5];
    
    // 현재 length 프로퍼티 값인 5보다 작은 숫자 값 3을 length 프로퍼티에 할당
    arr.length = 3;
    
    // 배열의 길이가 5에서 3으로 줄어든다.
    console.log(arr); // [1, 2, 3]
  • 주의할 점은 현재 length 프로퍼티의 값보다 큰 숫자를 할당하는경우다.

  • length의 값은 변경되지만 실제 배열의 길이는 늘어나지 않는다.

    const arr = [1];
    
    // 현재 length 프로퍼티 값인 1보다 큰 숫자 값 3을 length 프로퍼티에 할당
    arr.length = 3;
    
    // length 프로퍼티 값은 변경되지만 실제로 배열의 길이가 늘어나지는 않는다.
    console.log(arr.length); // 3
    console.log(arr); // [1, empty × 2]
    console.log(Object.getOwnPropertyDescriptors(arr));
    /*
    {
      '0': {value: 1, writable: true, enumerable: true, configurable: true},
      length: {value: 3, writable: true, enumerable: false, configurable: false}
    }
    */
  • 자바스크립트의 배열은 희소배열이기 때문에, 다음과 같은 방식도 가능하다.

    // 희소 배열
    const sparse = [, 2, , 4];
    
    // 희소 배열의 length 프로퍼티 값은 요소의 개수와 일치하지 않는다.
    console.log(sparse.length); // 4
    console.log(sparse); // [empty, 2, empty, 4]
    
    // 배열 sparse에는 인덱스가 0, 2인 요소가 존재하지 않는다.
    console.log(Object.getOwnPropertyDescriptors(sparse));
    /*
    {
      '1': { value: 2, writable: true, enumerable: true, configurable: true },
      '3': { value: 4, writable: true, enumerable: true, configurable: true },
      length: { value: 4, writable: true, enumerable: false, configurable: false }
    }
    */
  • 희소배열은 length와 배열 요소의 개수가 일치하지 않는다.

  • 희소 배열의 length는 희소배열의 실제 요소 개수보다 언제나 크다.

  • 배열을 생성할 경우, 희소 배열을 생성하지 않도록 주의하자. 배열에는 같은 타입의 요소를 연속적으로 위치시키는 것이 최선

27.4 배열 생성

27.4.1 배열 리터럴

  • 가장 일반적이고 간편한 배열 생성방식 = 배열 리터럴

    const arr = [1, 2, 3];
    console.log(arr.length); // 3
  • 배열 리터럴에 요소를 하나도 추가하지 않으면 배열의 길이, length 프로퍼티 값이 0인 빈배열이 된다.

    const arr = [];
    console.log(arr.length); // 0
  • 배열 리터럴에 요소를 생략하면 희소 배열이 된다.

    const arr = [1, , 3]; // 희소 배열
    
    // 희소 배열의 length는 배열의 실제 요소 개수보다 언제나 크다.
    console.log(arr.length); // 3
    console.log(arr);        // [1, empty, 3]
    console.log(arr[1]);     // undefined

27.4.2 Array 생성자 함수

  • Array 생성자 함수를 통해 배열을 생성할 수 있다.

  • Array 생성자 함수는 전달된 인수의 개수에 따라 다르게 동작한다.

    • 전달된 인수가 1개이고 숫자인 경우 length 프로퍼티 값이 인수인 배열 생성.

      const arr = new Array(10);
      
      console.log(arr); // [empty × 10]
      console.log(arr.length); // 10
    • 이때 생성된 배열은 희소배열이다.

      console.log(Object.getOwnPropertyDescriptors(arr));
      /*
      {
        length: {value: 10, writable: true, enumerable: false, configurable: false}
      }
      */
    • 배열은 요소를 최대 232-1(4,294,967,295)개 가질 수 있다. 전달된 인수가 0보다 작거나 232-1(4,294,967,296 - 1) 이하여야 한다. 범위를 벗어나면 RangeError가 발생한다.

      // 배열은 요소를 최대 4,294,967,295개 가질 수 있다.
      new Array(4294967295);
      
      // 전달된 인수가 0 ~ 4,294,967,295를 벗어나면 RangeError가 발생한다.
      new Array(4294967296); // RangeError: Invalid array length
      
      // 전달된 인수가 음수이면 에러가 발생한다.
      new Array(-1); // RangeError: Invalid array length
    • 전달된 인수가 없는 경우 빈 배열을 생성한다. 배열 리터럴 []과 같다.

      new Array(); // -> []
    • 전달된인수가 2개 이상이거나 숫자가 아닌 경우 인수를 요소로 갖는 배열을 생성한다.

      // 전달된 인수가 2개 이상이면 인수를 요소로 갖는 배열을 생성한다.
      new Array(1, 2, 3); // -> [1, 2, 3]
      
      // 전달된 인수가 1개지만 숫자가 아니면 인수를 요소로 갖는 배열을 생성한다.
      new Array({}); // -> [{}]
    • Array 생성자 함수는 new 연산자와 함께 호출하지 않더라도(일반 함수로서 호출해도) 배열을 생성하는 생성자 함수로 동작한다. 이는 Array 생성자 함수 내부에서 new.target을 확인하기 때문이다.

      Array(1, 2, 3); // -> [1, 2, 3]

27.4.3 Array.of

  • ES6에서 도입된 Array.of 메서드는 전달된 인수를 요소로 갖는 배열을 생성한다.

  • 인수가 1개이고 숫자이더라도 인수를 요소로 갖는 배열을 생성한다.

    // 전달된 인수가 1개이고 숫자이더라도 인수를 요소로 갖는 배열을 생성한다.
    Array.of(1); // -> [1]
    
    Array.of(1, 2, 3); // -> [1, 2, 3]
    
    Array.of('string'); // -> ['string']

27.4.4 Array.from

  • ES6에서 도입된 Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 인수로 전달받아 배열로 변환하여 반환한다.

    // 유사 배열 객체를 변환하여 배열을 생성한다.
    Array.from({ length: 2, 0: 'a', 1: 'b' }); // -> ['a', 'b']
    
    // 이터러블을 변환하여 배열을 생성한다. 문자열은 이터러블이다.
    Array.from('Hello'); // -> ['H', 'e', 'l', 'l', 'o']
  • Array.from 메서드를 사용하면 두 번째 인수로 전달한 콜백함수를 통해 값을 만들면서 요소를 채울 수 있다.

  • Array.from 메서드는 두 번째 인수로 전달한 콜백 함수에 첫 번째 인수에 의해 생성된 배열의 요소 값과 인덱스를 순차적으로 전달하면서 호출하고, 콜백 함수의 반환값으로 구성된 배열을 반환한다.

    // Array.from에 length만 존재하는 유사 배열 객체를 전달하면 undefined를 요소로 채운다.
    Array.from({ length: 3 }); // -> [undefined, undefined, undefined]
    
    // Array.from은 두 번째 인수로 전달한 콜백 함수의 반환값으로 구성된 배열을 반환한다.
    Array.from({ length: 3 }, (_, i) => i); // -> [0, 1, 2]

유사 배열 객체와 이터러블 객체

유사 배열 객체는 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고 length 프로퍼티를 갖는 객체를 말한다.

유사 배열 객체는 마치 배열처럼 for문으로 순회할 수 있다.

// 유사 배열 객체
const arrayLike = {
  '0': 'apple',
  '1': 'banana',
  '2': 'orange',
  length: 3
};

// 유사 배열 객체는 마치 배열처럼 for 문으로 순회할 수도 있다.
for (let i = 0; i < arrayLike.length; i++) {
  console.log(arrayLike[i]); // apple banana orange
}

이터러블 객체는 Symbol.iterator 메서드를 구현하여 for ... of 문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있는 객체를 말한다. 뒤 34장 "이터러블"에서 자세하게 다룬다.

27.5 배열 요소의 참조

  • 배열 요소를 참조할 때 대괄호([]) 표기법을 사용한다. 대괄호 안에는 인덱스가 와야 한다.

    const arr = [1, 2];
    
    // 인덱스가 0인 요소를 참조
    console.log(arr[0]); // 1
    // 인덱스가 1인 요소를 참조
    console.log(arr[1]); // 2
  • 존재하지 않는 요소에 접근하면 undefined가 반환된다.

    const arr = [1, 2];
    
    // 인덱스가 2인 요소를 참조. 배열 arr에는 인덱스가 2인 요소가 존재하지 않는다.
    console.log(arr[2]); // undefined
  • 배열은 사실 인덱스를 나타내는 문자열을 프로퍼티 키로 갖는 객체다. 희소 배열의 존재하지 않는 요소를 참조해도 undefined를 반환한다.

    // 희소 배열
    const arr = [1, , 3];
    
    // 배열 arr에는 인덱스가 1인 요소가 존재하지 않는다.
    console.log(Object.getOwnPropertyDescriptors(arr));
    /*
    {
      '0': {value: 1, writable: true, enumerable: true, configurable: true},
      '2': {value: 3, writable: true, enumerable: true, configurable: true},
      length: {value: 3, writable: true, enumerable: false, configurable: false}
    */
    
    // 존재하지 않는 요소를 참조하면 undefined가 반환된다.
    console.log(arr[1]); // undefined
    console.log(arr[3]); // undefined

27.6 배열 요소의 추가와 갱신

  • 배열도 객체처럼 요소를 동적으로 추가할 수 있다. 존재하지 않는 인덱스를 사용해 값을 할당하면 새로운 요소가 추가된다.

    const arr = [0];
    
    // 배열 요소의 추가
    arr[1] = 1;
    
    console.log(arr); // [0, 1]
    console.log(arr.length); // 2
  • 만약 현재 배열의 length 프로퍼티 값보다 큰 인덱스로 새로운 요소를 추가하면 희소 배열이 된다.

    arr[100] = 100;
    
    console.log(arr); // [0, 1, empty × 98, 100]
    console.log(arr.length); // 101
    // 명시적으로 값을 할당하지 않은 요소는 생성되지 않는다.
    console.log(Object.getOwnPropertyDescriptors(arr));
    /*
    {
      '0': {value: 0, writable: true, enumerable: true, configurable: true},
      '1': {value: 1, writable: true, enumerable: true, configurable: true},
      '100': {value: 100, writable: true, enumerable: true, configurable: true},
      length: {value: 101, writable: true, enumerable: false, configurable: false}
    */
  • 이미 요소가 존재하는 요소에 값을 재할당하면 요소값이 갱신된다.

    // 요소값의 갱신
    arr[1] = 10;
    
    console.log(arr); // [0, 10, empty × 98, 100]
  • 인덱스는 요소의 위치를 나타내므로 반드시 0 이상의 정수(또는 정수형태의 문자열)을 사용해야 한다. 정수 이외의 값을 인덱스처럼 사용하면 프로퍼티가 생성된다. 이때 추가된 프로퍼티는 length 프로퍼티 값에 영향을 주지 않는다.

    const arr = [];
    
    // 배열 요소의 추가
    arr[0] = 1;
    arr['1'] = 2;
    
    // 프로퍼티 추가
    arr['foo'] = 3;
    arr.bar = 4;
    arr[1.1] = 5;
    arr[-1] = 6;
    
    console.log(arr); // [1, 2, foo: 3, bar: 4, '1.1': 5, '-1': 6]
    
    // 프로퍼티는 length에 영향을 주지 않는다.
    console.log(arr.length); // 2

27.7 배열 요소의 삭제

  • 배열은 사실 객체이기 때문에 배열의 특정 요소를 삭제하기 위해 delete 연산자를 사용할 수 있다.

    const arr = [1, 2, 3];
    
    // 배열 요소의 삭제
    delete arr[1];
    console.log(arr); // [1, empty, 3]
    
    // length 프로퍼티에 영향을 주지 않는다. 즉, 희소 배열이 된다.
    console.log(arr.length); // 3
  • delete 연산자는 객체의 프로퍼티를 삭제한다. 따라서 위 예제의 delete arr[1]은 arr에서 프로퍼티 키가 '1'인 프로퍼티를 삭제한다. 따라서 length 프로퍼티 값에 영향을 주지 못한다 => 희소 배열

  • 희소 배열을 만들지 않으면서 배열의 특정 요소를 완전히 삭제하려면 Array.prototype.splice 메서드를 사용한다.

    const arr = [1, 2, 3];
    
    // Array.prototype.splice(삭제를 시작할 인덱스, 삭제할 요소 수)
    // arr[1]부터 1개의 요소를 제거
    arr.splice(1, 1);
    console.log(arr); // [1, 3]
    
    // length 프로퍼티가 자동 갱신된다.
    console.log(arr.length); // 2

27.8 배열 메서드

  • 배열 메서드는 결과물을 반환하는 패턴이 두 가지이므로 주의가 필요하다.

    • 배열 원본을 직접 변경하는 메서드
    • 원본 배열을 직접 변경하지 않고 새로운 배열을 생성하여 반환하는 메서드
    const arr = [1];
    
    // push 메서드는 원본 배열(arr)을 직접 변경한다.
    arr.push(2);
    console.log(arr); // [1, 2]
    
    // concat 메서드는 원본 배열(arr)을 직접 변경하지 않고 새로운 배열을 생성하여 반환한다.
    const result = arr.concat(3);
    console.log(arr);    // [1, 2]
    console.log(result); // [1, 2, 3]
  • ES5부터 도입된 배열 메서드는 대부분 원본 배열을 직접 변경하지 않지만, 초창기 배열 메서드는 원본 배열을 직접 변경하는 경우가 많다.

27.8.1 Array.isArray

  • Array.isArray 메서드는 전달된 인수가 배열이면 true, 배열이 아니면 false를 반환한다.

    // true
    Array.isArray([]);
    Array.isArray([1, 2]);
    Array.isArray(new Array());
    
    // false
    Array.isArray();
    Array.isArray({});
    Array.isArray(null);
    Array.isArray(undefined);
    Array.isArray(1);
    Array.isArray('Array');
    Array.isArray(true);
    Array.isArray(false);
    Array.isArray({ 0: 1, length: 1 })

27.8.2 Array.prototype.indexOf

  • indexOf 메서드는 원본 배열에서 인수로 전달된 요소를 검색하여 인덱스를 반환한다.

    const arr = [1, 2, 2, 3];
    
    // 배열 arr에서 요소 2를 검색하여 첫 번째로 검색된 요소의 인덱스를 반환한다.
    arr.indexOf(2);    // -> 1
    // 배열 arr에 요소 4가 없으므로 -1을 반환한다.
    arr.indexOf(4);    // -> -1
    // 두 번째 인수는 검색을 시작할 인덱스다. 두 번째 인수를 생략하면 처음부터 검색한다.
    arr.indexOf(2, 2); // -> 2
  • indexOf 메서드는 배열에 특정 요소가 있는지 확인할 때 유용하다.

    const foods = ['apple', 'banana', 'orange'];
    
    // foods 배열에 'orange' 요소가 존재하는지 확인한다.
    if (foods.indexOf('orange') === -1) {
      // foods 배열에 'orange' 요소가 존재하지 않으면 'orange' 요소를 추가한다.
      foods.push('orange');
    }
    
    console.log(foods); // ["apple", "banana", "orange"]
  • indexOf 메서드 대신 ES7에서 도입된 Array.prototype.includes 메서드를 사용하면 가독성이 더 좋다.

    const foods = ['apple', 'banana'];
    
    // foods 배열에 'orange' 요소가 존재하는지 확인한다.
    if (!foods.includes('orange')) {
      // foods 배열에 'orange' 요소가 존재하지 않으면 'orange' 요소를 추가한다.
      foods.push('orange');
    }
    
    console.log(foods); // ["apple", "banana", "orange"]

27.8.3 Array.prototype.push

  • push 메서드는 인수로 전달받은 모든 값을 원본 배열의 마지막 요소로 추가하고 변경된 length 프로퍼티의 값을 반환한다. push 메서드는 원본 배열을 직접 변경한다.

    const arr = [1, 2];
    
    // 인수로 전달받은 모든 값을 원본 배열 arr의 마지막 요소로 추가하고 변경된 length 값을 반환한다.
    let result = arr.push(3, 4);
    console.log(result); // 4
    
    // push 메서드는 원본 배열을 직접 변경한다.
    console.log(arr); // [1, 2, 3, 4]
  • push 메서드는 성능 면에서 좋지 않기 때문에, 마지막에 추가할 요소가 하나뿐이라면 push 메서드를 사용하지 않고 length 프로퍼티를 사용하여 배열의 마지막에 요소를 추가할 수 있다.

    const arr = [1, 2];
    
    // arr.push(3)과 동일한 처리를 한다. 이 방법이 push 메서드보다 빠르다.
    arr[arr.length] = 3;
    console.log(arr); // [1, 2, 3]
  • push 메서드는 원본 배열을 직접 변경하는 부수 효과가 있기 때문에 ES6 스프레드 문법을 사용하는 편이 좋다.

    const arr = [1, 2];
    
    // ES6 스프레드 문법
    const newArr = [...arr, 3];
    console.log(newArr); // [1, 2, 3]

27.8.4 Array.prototype.pop

  • pop 메서드는 원본 배열에서 마지막 요소를 제거하고 제거한 요소를 반환한다. 원본 배열이 빈 배열이면 undefined를 반환한다.

  • pop 메서드는 원본 배열을 직접 변경한다.

    const arr = [1, 2];
    
    // 원본 배열에서 마지막 요소를 제거하고 제거한 요소를 반환한다.
    let result = arr.pop();
    console.log(result); // 2
    
    // pop 메서드는 원본 배열을 직접 변경한다.
    console.log(arr); // [1]
  • pop 메서드와 push 메서드를 활용하면 stack을 쉽게 구현할 수 있다.

    const Stack = (function () {
      function Stack(array = []) {
        if (!Array.isArray(array)) {
          // "47. 에러 처리" 참고
          throw new TypeError(`${array} is not an array.`);
        }
        this.array = array;
      }
    
      Stack.prototype = {
        // "19.10.1. 생성자 함수에 의한 프로토타입의 교체" 참고
        constructor: Stack,
        // 스택의 가장 마지막에 데이터를 밀어 넣는다.
        push(value) {
          return this.array.push(value);
        },
        // 스택의 가장 마지막 데이터, 즉 가장 나중에 밀어 넣은 최신 데이터를 꺼낸다.
        pop() {
          return this.array.pop();
        },
        // 스택의 복사본 배열을 반환한다.
        entries() {
          return [...this.array];
        }
      };
    
      return Stack;
    }());
    
    const stack = new Stack([1, 2]);
    console.log(stack.entries()); // [1, 2]
    
    stack.push(3);
    console.log(stack.entries()); // [1, 2, 3]
    
    stack.pop();
    console.log(stack.entries()); // [1, 2]
  • stack을 클래스로 구현하면 다음과 같다.

    class Stack {
      #array; // private class member
    
      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];
      }
    }
    
    const stack = new Stack([1, 2]);
    console.log(stack.entries()); // [1, 2]
    
    stack.push(3);
    console.log(stack.entries()); // [1, 2, 3]
    
    stack.pop();
    console.log(stack.entries()); // [1, 2]

27.9.2 Array.prototype.forEach

  • 함수형 프로그래밍

    • 순수 함수와 보조 함수의 조합

      • 조건문 과 반복문을 제거하여 복잡성을 해결
      • 변수의 사용을 억제하여 상태 변경을 피함
    • for문 : 함수형 프로그래밍이 추구하는 바와 맞지 않음

      • 반복을 위한 변수를 선언
      • 조건식과 증감식
      const numbers = [1, 2, 3]; 
      const pows = [];
      
      // for 문으로 배열 순회
      for (let i = 0; i < numbers.length; i++) {
        pows.push(numbers[i] ** 2);
      }
      console.log(pows); // [1, 4, 9]
  • forEach 메서드

    • for문을 대체할 수 있는 반복문을 추상화한 고차 함수
    • 내부에서 반복문을 통해 자신을 호출한 배열을 순회 하면서 수행 해야할 처리를 콜백 함수로 전달 받아 반복 호출
    const numbers = [1, 2, 3]; 
    const pows = [];
    
    // forEach 메서드는 numbers 배열의 모든 요소를 순회하면서 콜백 함수를 반복 호출한다.
    numbers.forEach(item => pows.push(item ** 2)); 
    console.log(pows); // [1, 4, 9]
    • numbers 배열의 요소가 3개이므로 콜백 함수도 3번 호출
    // forEach 메서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다. 
    [1, 2, 3].forEach((item, index, arr) => {
    	console.log(`요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringify(arr)}`); 
    }); //JSON.stringify 메서드는 객체를 JSON 포맷의 문자열로 변환
    /*
    요소값: 1, 인덱스: 0, this: [1,2,3]
    요소값: 2, 인덱스: 1, this: [1,2,3]
    요소값: 3, 인덱스: 2, this: [1,2,3) 
    */
    • 호출한 배열의 요소값, 인덱스, forEach 메서드를 호출한 배열(this)을 순차적으로 전달
    const numbers = [1, 2, 3];
    // forEach 메서드는 원본 배열을 변경하지 않지만 콜백 함수를 통해 원본 배열을 변경할 수는 있다.
    // 콜백 함수의 세 번째 매개변수 arr은 원본 배열 numbers를 가리킨다.
    // 따라서 콜백 함수의 세 번째 매개변수 arr을 직접 변경하면 원본 배열 numbers가 변경된다. 
    
    numbers.forEach((item, index, arr) => { arr[index] = item ** 2; }); console.log(numbers); // (1, 4, 9)
    • forEach 메서드는 원본 배열(forEach 메서드를 호출한 배열, 즉 this)을 변경하지 않지만, 콜백 함수를 통해 원본을 변경할 수 있음
    const result = [1, 2, 3].forEach(console.log); 
    console.log(result); // undefined
    • forEach 메서드의 반환값은 언제나 undefined
  • forEach 메서드의 두 번째 인수로 forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달할 수 있음

    class Numbers { 
      numberArray = [];
     	multiply(arr) { 
        arr.forEach(function (item) {
    			// TypeError: Cannot read property 'numberArray' of undefined
    			this.numberArray.push(item * item); 
      	});
      } 
    }
    
    const numbers = new Numbers(); 
    numbers.multiply([1, 2, 3]);
    • forEach 메서드의 콜백 함수는 일반 함수로 호출되므로 콜백 함수 내부의 this는 undefined를 가리킴
    • this가 전역 객체가 아닌 undefined를 가리키는 이유는 클래스 내부의 모든 코드에는 암묵적으로 strict mode가 적용되기 때문
    class Numbers { 
      numberArray = [];
    
    	multiply(arr) { 
      	arr.forEach(function (item) {
        	this.numberArray.push(item * item);
     	 	}, this); // forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달 
      }
    }
    
    const numbers = new Numbers(); 
    numbers.multiply([1, 2, 3]);
    console.log(numbers.numberArray); // [1, 4, 9]
    • forEach 메서드의 콜백 함수 내부의 this와 multiply 메서드 내부의 this를 일치시키려면 forEach 메서드 의 두 번째 인수로 forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달
    • 더나은방법은 ES6의 화살표함수를사용
      • 화살표 함수는 함수 자체의 this 바인딩을 갖지 않음
      • 화살표 함수 내부에서 this를 참조하면 상위 스코프, 즉 multiply 메서드 내부의 this를 그대로 참조
    class Numbers { 
      numberArray = [];
    
    	multiply(arr) { 
      	// 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다.
    		arr.forEach(item => this.numberArray.push(item * item));
      }
    }
    
    const numbers = new Numbers(); 
    numbers.multiply([1, 2, 3]);
    console.log(numbers.numberArray); // [1, 4, 9]
  • forEach 메서드의 폴리필

    // 만약 Array.prototype에 forEach 메서드가 존재하지 않으면 폴리필을 추가한다. 
    if (!Array.prototype.forEach) {	
      Array.prototype.forEach = function (callback, thisArg) {     
      // 첫 번째 인수가 함수가 아니면 TypeError를 발생시킨다.	
      if (typeof callback !== 'function') {		
        throw new TypeError(callback + ' is not a function'); }        
      // this로 사용할 두 번째 인수를 전달받지 못하면 전역 객체를 this로 사용한다.     
      thisArg = thisArg || window;    
      // for 문으로 배열을 순회하면서 콜백 함수를 호출한다.     
      for (var i = 0; i < this.length; i++) {			
        // call 메서드를 통해 thisArg를 전달하면서 콜백 함수를 호출한다.       
        // 이때 콜백 함수의 인수로 배열 요소, 인덱스, 배열 자신을 전달한다.       
        callback.call(thisArg, this[i], i, this);    
      }  
    };}
    • forEach 메서드도 내부에서는 반복문(for 문)을 통해 배열을 순회

      반복문을 메서 드 내부로 은닉하여 로직의 흐름을 이해하기 쉽게 하고 복잡성을 해결

  • break,continue문을 사용할 수 없음

    [1, 2, 3].forEach(item => {   console.log(item);  if (item > 1) break; 
                               // SyntaxError: Illegal break statement 
                              });
                               [1, 2, 3].forEach(item => {	console.log(item);	if (item > 1) continue;	
                                                          // SyntaxError: Illegal continue statement: no surrounding iteration statement });
  • 희소배열의 경우 존재하지 않는 요소는 순회대상에서 제외 (map. filter, reduce 메서드 등에서도 마찬가지)

    • ? 폴리필에서는 그런내용은 없는거 같은데
    // 희소 배열
    const arr = [1, , 3];
    // for 문으로 희소 배열을 순회
    for (let i = 0; i < arr.length; i++) {  console.log(arr[i]); 
                                      // 1, undefined, 3 }// forEach 메서드는 희소 배열의 존재하지 않는 요소를 순회 대상에서 제외한다. 
                                          arr.forEach(v => console.log(v)); // 1, 3
  • forEach 메서드는 for 문에 비해 성능은 좋지 않지만 가독성이 더 좋음

27.9.3 Array.prototype.map

  • 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출

  • 콜백함수의 반환값들로 구성된 새로운 배열을 반환

    const numbers = [1, 4, 9];// map 메서드는 numbers 배열의 모든 요소를 순회하면서 콜백 함수를 반복 호출한다. // 그리고 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.const roots = numbers.map(item => Math.sqrt(item));// 위 코드는 다음과 같다.// const roots = numbers.map(Math.sqrt);;// map 메서드는 새로운 배열을 반환한다.console.log(roots); // [ 1, 2, 3] // map 메서드는 원본 배열을 변경하지 않는다. console.log(numbers); // [ 1, 4, 9 ]
  • forEach vs map

    • forEach : 단순히 반복 문을 대체

    • map : 요소값을 다른 값으로 매핑mapping한 새로운 배열을 생성

      image-20211010172152437

  • 콜백함수의 인자로 (요소값, 인덱스, 호출한 배열 this) 를 순차적으로 전달

    // map 에서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다. 
    [1, 2,3].map((item, index, arr) => {
      console.log( `요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringify(arr)}` ); 
    	return item;
    });
    
     /*
    요소값: 1, 인덱스: 0, this: [1,2,3] 
    요소값: 2, 인덱스: 1, this: [1,2,3] 
    요소값: 3, 인덱스: 2, this: [1,2,3]
    */
  • map 메서드의 두 번째 인수로 map 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달

    class Prefixer {
      constructor(prefix) { 
        this.prefix = prefix;
      }
      
      add(arr) {
        return arr.map(function (item) {
          // 외부에서 this를 전달하지 않으면 this는 undefined를 가리킨다. 
          return this.prefix + item;
        }, this); // map 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달 
      }
    }
      
    const prefixer = new Prefixer('-webkit-');
    console.log(prefixer.add(['transition', 'user-select'])); 
    // ['-webkit-transition', '-webkit-user-select']
  • 화살표 함수를 사용하면, this 참조가 가능하다.

    class Prefixer {
      constructor(prefix) { 
        this.prefix = prefix;
      }
      
      add(arr) {
        // 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다.
        return arr.map(item => this.prefix + item);
      }
    }
      
    const prefixer = new Prefixer('-webkit-');
    console.log(prefixer.add(['transition', 'user-select'])); 
    // ['-webkit-transition', '-webkit-user-select']

27.9.4 Array.prototype.filter

  • 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환
const numbers = [1, 2, 3, 4. 5];

// filter 메서드는 numbers 배열의 모든 요소를 순회하면서 콜백 함수를 반복 호출한다. 
// 그리고 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환한다.
// 다음의 경우 numbers 배열에서 홀수인 요소만 필터링한다(1은 true로 평가된다).
const odds = numbers.filter(item => item % 2); 
console.log(odds); // [1, 3, 5]

image-20211010175804000

  • 콜백함수의 인자로 (요소값, 인덱스, 호출한 배열 this) 를 순차적으로 전달
// filter 메서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달한다. 
[1, 2, 3].filter((item, index, arr) => {
  console.log(요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringify(arr)});
  return item % 2; 
});
/*
요소값: 1, 인덱스: 0, this: [1,2,3]
요소값: 2, 인덱스: 1, this: [1,2,3]
요소값: 3, 인덱스: 2, this: [1,2,3] 
*/
  • 자신을 호출한 배열에서 특정요소를 제거
class Users { 
  constructor() {
		this.users - [
			{ id: 1, name: 'Lee' }, 
      { id: 2, name: 'Kim' }
		]; 
  }
	
  // 요소 추출 
  findById(id) {
  	// id가 일치하는 사용자만 반환한다.
    return this.users.filter(user => user.id === id); 
  }
  
  // 요소 제거
  remove(id) {
    // id가 일치하지 않는 사용자를 제거한다.
     this.users = this.users.filter(user => user.id !== id);
  }
}
const users = new Users();

let user = users.findById(1); 
console.log(user); // [{ id: 1, name: 'Lee' }]

// id가 1인 사용자를 제거한다. 
users.remove(1);
user = users.findById(1); 
console.log(user); // []
  • 특정 요소가 중복되어 있다면 중복된 요소가 모두 제거
    • 특정 요소를 하나만 제거하려면 indexOf 메서드를 통해 특정요소의 인덱스를 취득한 다음 splice메서드를 사용

27.9.5 Array.prototype.reduce

  • 콜백함수의 반환값을 다음 순회시에 콜백함수의 첫번째 인수로 전달하면서, 콜백함수를 호출하여 하나의 결과값을 만들어 반환

    • reduce의 두개의 인수
      • 1 : 콜백 함수
      • 2 : 초기값
    • reduce 메서드의 콜백 함수에는 4개의 인수
      • 1: 초기값 또는 콜백함수의 이전반환 값
      • 2 : reduce메서드를 호출한 배열의 요소값
      • 3 : 인덱스
      • 4 : reduce 메서드를 호출한 배열 자체, 즉 this
    // 1부터 4까지 누적을 구한다.
    const sum = (1, 2, 3, 4].reduces(accumulator, currentValue, index, array) => accumula
    tor + currentValue, 0); 
    
    console.log(sum); // 10

    image-20211010181013679

  • 평균 구하기

    const values = [1, 2, 3, 4, 5, 6];
    
    const average = values.reduce((acc, cur, i, { length }) => {
    // 마지막 순회가 아니면 누적값을 반환하고 마지막 순회면 누적값으로 평균을 구해 반환한다. 
      return i === length - 1 ? (acc + cur) / length : acc + cur:
    }, 0);
    
    console.log(average); // 3.5
  • 최대값 구하기

    const values = [1, 2, 3, 4, 5];
    
    const max = values.reduce((acc, cur) => (acc > cur ? acc : cur), 0);
    console.log(max); // 5

    Math.max 를 사용하는게 더 직관적

  • 요소의 중복 횟수 구하기

    const fruits = ['banana', 'apple', 'orange', 'orange', 'apple'];
    
    const count = fruits.reduce((acc, cur) => {
      // 첫 번째 순회 시 acc는 초기값인 {}이고 cur은 첫 번째 요소인 'banana'다.
      // 초기값으로 전달받은 빈 객체에 요소값인 cur을 프로퍼티 키로, 요소의 개수를 프로퍼티 값으로 할당한다. 
      // 만약 프로퍼티 값이 undefined(처음 등장하는 요소)이면 프로퍼티 값을 1로 초기화한다.
      acc[cur] = (acc[cur] || 0) + 1;
      return acc;
    }, {});
    
     // 콜백 함수는 총 5번 호출되고 다음과 같이 결과값을 반환한다. 
    /*
    {banana: 1} => {banana: 1, apple: 1} => {banana: 1, apple: 1, orange: 1}
    => {banana: 1, apple: 1, orange: 2} => {banana: 1, apple: 2, orange: 2} 
    */
    
    console.log(count); // { banana: 1, apple: 2, orange: 2 }
  • 중첩 배열 평탄화

    const values = [1, [2, 3], 4, [5, 6]];
    
    const flatten = values.reduce((acc, cur) => acc.concat(cur), []);
    // [1] => [1, 2, 3] => [1, 2, 3, 4] => [1, 2, 3, 4, 5, 6]
    
    console.log(flatten); // [1, 2, 3, 4, 5, 6]
    • reduce 메서드보다 ES10(ECMAScript 2019)에서 도입된 Array.prototype. flat 메서드를 사용하는 방법
    [1, [2, 3, 4, 5]].flat(); // -> [1, 2, 3, 4, 5]
    
    // 인수 2는 중첩 배열을 평탄화하기 위한 깊이 값이다.
    [1, [2, 3, [4, 5]]].flat(2); // -> [1, 2, 3, 4, 5]
    27-120
  • 중복 요소 제거

    const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];
    
    const result = values.reduce((acc, cur, i, arr) => {
      // 순회 중인 요소의 인덱스가 자신의 인덱스라면 처음 순회하는 요소다.
      // 이 요소만 초기값으로 전달받은 배열에 담아 반환한다.
      // 순회 중인 요소의 인덱스가 자신의 인덱스가 아니라면 중복된 요소다.
      if (arr.indexOf(cur) === i) acc.push(cur);
      return acc;
    }, []);
    
    console.log(result); // [1, 2, 3, 5, 4]
    • filter가 더 직관적

      const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];
      
      // 순회중인 요소의 인덱스가 자신의 인덱스라면 처음 순회하는 요소이다. 이 요소만 필터링한다.
      const result = values.filter((v, i, arr) => arr.indexOf(v) === i);
      console.log(result); // [1, 2, 3, 5, 4]
    • Set을 사용할 수도 있음

      const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];
      
      // 중복을 허용하지 않는 Set 객체의 특성을 활용하여 배열에서 중복된 요소를 제거할 수 있다.
      const result = [...new Set(values)];
      console.log(result); // [1, 2, 3, 5, 4]

    reduce로 map, filter, some, every, find같은 배열의 고차함를 구현할 수 있음

  • 두번째 인수로 전달하는 초기값은 생략 가능하고, 생략하면 배열의 첫 번째 요소가 초기값이 된다.

    // reduce 메서드의 두 번째 인수, 즉 초기값을 생략했다.
    const sum = [1, 2, 3, 4].reduce((acc, cur) => acc + cur);
    console.log(sum); // 10

    image-20211010182416362

    • reduce 메서드를 호출할 때는 언제나 초기값을 전달하는 것이 안전

      const sum = [].reduce((acc, cur) => acc + cur);
      // TypeError: Reduce of empty array with no initial value
      • 빈 배열로 호출하게 되면 에러가 발생함
      const sum = [].reduce((acc, cur) => acc + cur, 0);
      console.log(sum); // 0
    • 특정 프로퍼티 값을 합산하는 경우

      const products = [
        { id: 1, price: 100 },
        { id: 2, price: 200 },
        { id: 3, price: 300 }
      ];
      
      // 1번째 순회 시 acc는 { id: 1, price: 100 }, cur은 { id: 2, price: 200 }이고
      // 2번째 순회 시 acc는 300, cur은 { id: 3, price: 300 }이다.
      // 2번째 순회 시 acc에 함수에 객체가 아닌 숫자값이 전달된다. 이때 acc.price는 undefined다.
      const priceSum = products.reduce((acc, cur) => acc.price + cur.price);
      
      console.log(priceSum); // NaN
      • 초기값을 반드시 전달해야 함
      const products = [
        { id: 1, price: 100 },
        { id: 2, price: 200 },
        { id: 3, price: 300 }
      ];
      
      /*
      1번째 순회 : acc => 0,   cur => { id: 1, price: 100 }
      2번째 순회 : acc => 100, cur => { id: 2, price: 200 }
      3번째 순회 : acc => 300, cur => { id: 3, price: 300 }
      */
      const priceSum = products.reduce((acc, cur) => acc + cur.price, 0);
      
      console.log(priceSum); // 600

27.9.6 Array.prototype.some

  • 콜백함수의반환값이단한번이라도참이면true,모두거짓이면false를반환

    // 배열의 요소 중에 10보다 큰 요소가 1개 이상 존재하는지 확인
    [5, 10, 15].some(item => item > 10); // -> true
    
    // 배열의 요소 중에 0보다 작은 요소가 1개 이상 존재하는지 확인
    [5, 10, 15].some(item => item < 0); // -> false
    
    // 배열의 요소 중에 'banana'가 1개 이상 존재하는지 확인
    ['apple', 'banana', 'mango'].some(item => item === 'banana'); // -> true
    
    // some 메서드를 호출한 배열이 빈 배열인 경우 언제나 false를 반환한다.
    [].some(item => item > 3); // -> false

27.9.7 Array.prototype.every

  • 콜백 함수의 반환값이 모두 참 이면 true, 단 한 번이라도 거짓이면 false를 반환

    // 배열의 모든 요소가 3보다 큰지 확인
    [5, 10, 15].every(item => item > 3); // -> true
    
    // 배열의 모든 요소가 10보다 큰지 확인
    [5, 10, 15].every(item => item > 10); // -> false
    
    // every 메서드를 호출한 배열이 빈 배열인 경우 언제나 true를 반환한다.
    [].every(item => item > 3); // -> true

27.9.8 Array.prototype.find

  • 콜백 함수를 호출 하여 반환값이 true인 첫 번째 요소를 반환

    const users = [
      { id: 1, name: 'Lee' },
      { id: 2, name: 'Kim' },
      { id: 2, name: 'Choi' },
      { id: 3, name: 'Park' }
    ];
    
    // id가 2인 첫 번째 요소를 반환한다. find 메서드는 배열이 아니라 요소를 반환한다.
    users.find(user => user.id === 2); // -> {id: 2, name: 'Kim'}

27.9.9 Array.prototype.findIndex

  • 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소의 인덱스를 반환

    const users = [
      { id: 1, name: 'Lee' },
      { id: 2, name: 'Kim' },
      { id: 2, name: 'Choi' },
      { id: 3, name: 'Park' }
    ];
    
    // id가 2인 요소의 인덱스를 구한다.
    users.findIndex(user => user.id === 2); // -> 1
    
    // name이 'Park'인 요소의 인덱스를 구한다.
    users.findIndex(user => user.name === 'Park'); // -> 3
    
    // 위와 같이 프로퍼티 키와 프로퍼티 값으로 요소의 인덱스를 구하는 경우
    // 다음과 같이 콜백 함수를 추상화할 수 있다.
    function predicate(key, value) {
      // key와 value를 기억하는 클로저를 반환
      return item => item[key] === value;
    }
    
    // id가 2인 요소의 인덱스를 구한다.
    users.findIndex(predicate('id', 2)); // -> 1
    
    // name이 'Park'인 요소의 인덱스를 구한다.
    users.findIndex(predicate('name', 'Park')); // -> 3

27.9.10 Array.prototype.flatMap

  • ES10(ECMAScript 2019)에서 도입된 flatMap 메서드로, map메서드를 통해 생성된 새로운 배열을 평탄화

    • map 메서드와 flat 메서드를 순차적으로 실행하는 효과
    const arr = ['hello', 'world'];
    
    // map과 flat을 순차적으로 실행
    arr.map(x => x.split('')).flat();
    // -> ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
    
    // flatMap은 map을 통해 생성된 새로운 배열을 평탄화한다.
    arr.flatMap(x => x.split(''));
    // -> ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
  • flatMap 메서드는 flat 메서드처럼 인수를 전달하여 평탄화 깊이를 지정할 수는 없고 1단계만 평탄화

    • 단계 지정이 필요하면, map과 flat을 사용
    const arr = ['hello', 'world'];
    
    // flatMap은 1단계만 평탄화한다.
    arr.flatMap((str, index) => [index, [str, str.length]]);
    // -> [[0, ['hello', 5]], [1, ['world', 5]]] => [0, ['hello', 5], 1, ['world', 5]]
    
    // 평탄화 깊이를 지정해야 하면 flatMap 메서드를 사용하지 말고 map 메서드와 flat 메서드를 각각 호출한다.
    arr.map((str, index) => [index, [str, str.length]]).flat(2);
    // -> [[0, ['hello', 5]], [1, ['world', 5]]] => [0, 'hello', 5, 1, 'world', 5]