Javascript

자바스크립트 배열 완전 정복: 희소 배열부터 다양한 메서드 활용까지

드리프트2 2024. 5. 17. 18:22

 

 

 

 

희소배열과 반복문(Sparse Arrays and Loops)

for-in 문은 프로퍼티를 반복하기 때문에, for-in 문을 사용하여 희소 배열(sparse array)을 반복할 때 프로퍼티가 없는(빈) 인덱스는 건너뜁니다.

공통된 3가지 for 문 특징

  • <문> 부분에 두 개 이상의 문(statement)이나 선언을 작성하려면 블록 문({ ... })을 사용해 하나의 문으로 묶어야 합니다.
  • for (...) { console.log("Hello"); console.log("World"); }
  • 하나의 문만 작성할 때도 블록 문을 사용할 수 있습니다.
  • for (...) { console.log("Hello"); }
  • for 문 자체는 하나의 문이므로 문이 기대되는 곳에 작성할 수 있습니다.
  • for 문을 중첩해서 사용할 수 있습니다.
  • for (...) for (...) ...;
  • 함수의 중괄호 안에 for 문을 작성할 수 있습니다.
    () => {
    for (...) ...;
    }
  • function foo() { for (...) ...; }

주의: () => for (...) ...;와 같이 작성할 수는 없습니다.

화살표 함수식에서 => 뒤에 {가 오지 않으면, =>의 오른쪽은 문이 아닌 표현식이 기대되기 때문입니다.

  • <문> 내부에서 break 문을 사용하면 루프 전체 실행을 종료할 수 있습니다.
    • break 문이 실행되면 현재 반복이 즉시 종료되고, 그 루프의 이후 반복은 실행되지 않습니다.
    • break 문이 실행되면 종료된 for 문의 바로 다음으로 실행이 이동합니다.
  • <문> 내부에서 continue 문을 사용하면 현재 반복만 종료할 수 있습니다.
    • continue 문이 실행되면 현재 반복이 즉시 종료되고, 다음 반복의 준비로 이동합니다.
    • continue 문이 실행되면 for 문의 <문> 부분 바로 다음으로 실행이 이동합니다.
    • 전통적인 for 문에서는 <업데이트> 표현식 평가로 이동합니다.

중첩을 깊게 하지 않고 각 반복에서 처리 대상을 좁힐 때 사용할 수 있습니다.

for (const article of articles) {
  if (article.isDraft) continue;
  if (article.lastUpdated < oneWeekAgo) continue;
  if (article.likes < 10) continue;

  console.log(article);

  // 중첩이 점점 깊어지지 않습니다.
  // if (!article.isDraft)
  //   if (article.lastUpdated >= oneWeekAgo)
  //     if (article.likes >= 10)
  //       console.log(article);
}
  • <문> 내부에서 return 문을 사용하면 for 문을 포함하는 함수 전체 실행을 종료할 수 있습니다.
    • return 문이 실행되면 현재 반복이 즉시 종료되고, 그 루프의 이후 반복은 실행되지 않습니다.
    • return 문이 실행되면 함수 전체가 종료되고 함수 호출자에게 실행이 돌아갑니다.
  • for 문을 포함한 문에는 레이블을 붙일 수 있습니다.
    • 레이블은 문의 바로 앞에 작성되며, 레이블 이름과 콜론(:)으로 구성됩니다.
    • break 문이나 continue 문에서 어느 문을 대상으로 할지 지정할 때 레이블을 사용할 수 있습니다.
      outer: for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
      if (i === 1 && j === 1) {
        break outer;
      }
      console.log(i, j);
      }
      }
  • continue 문에서 for, while, do-while 문이 아닌 문 레이블을 지정하면 구문 오류가 발생합니다.
  • 동일한 이름의 레이블을 중첩하면 구문 오류가 발생합니다.
  • a: for (;;) { a: for (;;) { // 구문 오류 // ... } }
b: for (;;) {
  // ...
}
b: for (;;) { // 중첩이 아니므로 구문 오류가 발생하지 않음
  // ...
}
  • 레이블이 없는 break 문은 가장 내부의 for, while, do-while, switch 문을 대상으로 합니다.
  • 레이블이 없는 continue 문은 가장 내부의 for, while, do-while 문을 대상으로 합니다.
  • 해당 문이 존재하지 않으면 구문 오류가 발생합니다.

while 문, do-while

while 문과 do-while 문은 반복 처리를 수행하는 기본적인 구문 중 하나입니다.

while

while (<조건>)
  <문>

while 문이 실행될 때는 다음과 같은 순서로 처리됩니다.

  1. <조건> 표현식이 평가됩니다.
  2. 평가 결과가 falsy이면 루프가 종료되고, 실행이 while 문 바로 다음으로 이동합니다.
  3. <문>이 실행됩니다.
  4. 1단계로 돌아갑니다.

일반적인 배열 반복은(억지로 작성해보면) 다음과 같은 형태입니다.

const array = ["a", "b", "c", "d", "e"];

let i = 0;
while (i < array.length) {
  console.log(array[i]);
  i++;
}
// 출력:
// a
// b
// c
// d
// e

do-while

do
  <문>
while (<조건>);

do-while 문이 실행될 때는 다음과 같은 순서로 처리됩니다.

  1. <문>이 실행됩니다.
  2. <조건> 표현식이 평가됩니다.
  3. 평가 결과가 falsy이면 루프가 종료되고, 실행이 do-while 문 바로 다음으로 이동합니다.
  4. 1단계로 돌아갑니다.

do-while 문은 첫 번째 1회는 조건 평가 없이 반드시 <문>이 실행된다는 점이 while 문과 다릅니다.

while 문, do-while 문의 특징

while 문과 do-while 문은 배열 반복 처리보다는 조건에 따라 반복 처리를 수행하는 데 중점을 둔 구문입니다.

구문적 특징은 for 문과 유사한 점이 많습니다.

  • <문>에 두 개 이상의 문을 작성하려면 블록 문({ ... })을 사용해 하나의 문으로 묶어야 합니다.
  • while 문과 do-while 문은 하나의 문이므로 문이 기대되는 곳에 작성할 수 있습니다.
  • continue, break, return 문에 대해서는 for 문과 동일한 동작을 합니다.
  • while 문과 do-while 문에도 레이블을 붙일 수 있습니다.

while 문, do-while 문의 변형

while (<조건>)
  <문>

for (; <조건>; )
  <문>

와 동일한 의미입니다.

do
  <문>
while (<조건>);

while (true) {
  <문>
  if (!(<조건>)) break;
}

와 동일한 의미입니다.

배열 메서드

JavaScript에는 배열을 다루기 위한 유용한 메서드들이 준비되어 있으며, 배열 요소에 대한 반복 처리를 위한 메서드도 있습니다.

반복 처리를 위한 배열 메서드의 대부분은 콜백 함수를 인수로 받습니다.

콜백 함수는 배열의 각 요소에 대해 실행되는 함수로, 반복적으로 수행할 작업을 기술합니다.

콜백 함수는 일반적으로 3개의 인수를 받습니다:

  • 요소: 배열 요소의 값
  • 인덱스: 요소의 인덱스
  • 배열: 반복 대상이 되는 전체 배열

forEach 메서드

배열의 forEach 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행합니다.

모든 요소에 대한 실행이 끝나면 forEach 메서드 호출이 종료되고 undefined를 반환합니다.

const array = ["a", "b", "c"];

array.forEach((element, i, obj) => {
  console.log(element, i, obj);
});
// 출력:
// a 0 [ 'a', 'b', 'c' ]
// b 1 [ 'a', 'b', 'c' ]
// c 2 [ 'a', 'b', 'c' ]

콜백 함수의 반환값은 무시되므로, 화살표 함수식의 내용이 표현식이면 중괄호를 생략하고 => 뒤에 직접 그 표현식을 써도 동일하게 동작합니다.

const array = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
const result = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

array.forEach((element) => {
  result[element] += 1;
});

// 아래와 동일한 동작
array.forEach((element) => result[element] += 1);

forEach 메서드의 특징

배열의 forEach 메서드는 반복 처리에 의미 있는 결과를 반환하지 않으므로(항상 undefined를 반환), 반복 처리가 의미를 가지려면 콜백 함수 내에서 부작용을 일으키는 처리를 해야 합니다.

위의 예제에서는 console.log로 출력하거나 result 객체의 프로퍼티를 변경하는 것이 부작용에 해당합니다.

콜백 함수 내에서 루프 전체를 중간에 종료하고 루프 바로 다음으로 처리 흐름을 이동시키는(for 문에서의 break 문이나 return 문에 해당) 수단은 없습니다.

(forEach 메서드의 실행을 종료하기 위해 에러를 던질 수 있지만, forEach 메서드를 호출하는 문을 try-catch 문으로 둘러싸 에러를 포착해야 합니다.)

현재 반복을 중간에 종료하고 다음 반복으로 넘어가기 위해서는(for 문에서의 continue 문에 해당) return 문을 사용합니다.

이러한 return 문은 각 반복에서 처리 대상을 좁힐 때 중첩을 깊게 하지 않기 위해 사용되기도 합니다.

(이 테크닉은 '조기 반환(Early Return)'이라고 불리며, 반복 처리뿐만 아니라 일반 함수에서도 사용됩니다.)

articles.forEach((article) => {
  if (article.isDraft) return;
  if (article.lastUpdated < oneWeekAgo) return;
  if (article.likes < 10) return;

  console.log(article);

  // 아래와 같이 중첩이 깊어지지 않습니다.
  // if (!article.isDraft)
  //   if (article.lastUpdated >= oneWeekAgo)
  //     if (article.likes >= 10)
  //       console.log(article);
});

인수 변수에 값을 할당해도 원래 배열 요소에 영향을 주지 않습니다.

const array = [1, 2, 3, 4, 5];
array.forEach((element) => {
  // 이 할당은 콜백 함수 내의 변수 element만 변경하며,
  // 반복 대상인 배열 array의 내용에는 영향을 주지 않음!
  element = element * 2;
});
console.log(array);
// 출력: [ 1, 2, 3, 4, 5 ]

인수 분해 할당이 가능합니다.

const users = [
  { name: "Alice", age: 20 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 30 },
];
users.forEach(({ name, age }) => {
  console.log(name, age);
});

forEach 메서드로 반복 처리할 때 비동기 처리를 순차적으로 실행할 수 없습니다.

비동기 처리를 순차적으로 실행하려면 for-of 문과 await 연산자를 사용해 반복 처리를 하거나 Promise 메서드를 사용해야 합니다.

const result = [];
const registerElement = async (element) => {
  // 1초가 걸리는 비동기 처리를 재현
  await new Promise((resolve) => setTimeout(resolve, 1000));
  result.push(element);
};

const array = ["a", "b", "c", "d", "e"];
// 이전 `registerElement` 처리가 끝나기 전에 다음 `registerElement` 처리가 시작됨!
array.forEach(async (element) => {
  await registerElement(element);
});
// 또한 5번의 `registerElement` 처리는 이 시점에서 어느 것도 끝나지 않음!
console.log(result); // 출력: []

대안:

const array = ["a", "b", "c", "d", "e"];
for (const element of array) {
  await registerElement(element);
  // 이 시점에서 이 `registerElement` 처리는 끝난 상태
}
console.log(result); // 출력: [ 'a', 'b', 'c', 'd', 'e' ]
const array = ["a", "b", "c", "d", "e"];
let promise = Promise.resolve();
array.forEach((element) => {
  // 이전 `registerElement` 처리가 끝난 후 다음 `registerElement` 처리가 시작되도록 함
  promise = promise.then(async () => {
    await registerElement(element);
  });
});
await promise; // 마지막 `registerElement` 처리가 끝날 때까지 기다림
console.log(result); // 출력: [ 'a', 'b', 'c', 'd', 'e' ]
const array = ["a", "b", "c", "d", "e"];
await array.reduce(
  (prev, element) => prev.then(async () => {
    await registerElement(element);
  }),
  Promise.resolve()
);
console.log(result); // 출력: [ 'a', 'b', 'c', 'd', 'e' ]

반복 대상이 되는 인덱스

forEach 메서드가 반복하는 범위는 메서드가 호출될 때의 배열 길이를 기준으로 결정됩니다.

따라서 forEach 메서드의 콜백 함수에서 배열의 길이를 늘린 경우, 원래 길이를 초과하는 인덱스의 요소는 반복되지 않습니다.

희소 배열(sparse array)의 프로퍼티가 없는(빈) 인덱스는 반복되지 않습니다.

const array = ["a", , "c"];
array.forEach((element, i) => {
  console.log(i, element);
});
// 출력:
// 0 a
// 2 c

콜백 함수의 세 번째 인수

콜백 함수의 세 번째 인수는 메서드 체인의 끝에서 forEach 메서드를 호출할 때 유용할 수 있습니다.

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const twinPrimes = [];
array
  .filter((element) => isPrime(element)) // 소수만 추출
  .forEach((element, i, primes) => {
    // 세 번째 인수 primes의 값은 반복 대상인 배열, 즉 소수만 추출된 배열(array가 아닌 array.filter(...))

    if (i === 0) return; // 첫 번째 요소는 바로 앞의 소수가 없으므로 건너뜀
    const prev = primes[i - 1]; // 바로 앞의 소수를 primes에서 가져옴
    // 쌍둥이 소수인지 판별
    if (element - prev === 2) {
      twinPrimes.push([prev, element]);
    }
  });

console.log(twinPrimes);
// 출력: [ [ 3, 5 ], [ 5, 7 ] ]

forEach 메서드의 두 번째 인수

forEach 메서드의 두 번째 인수로 콜백 함수에서 this로 사용될 값을 지정할 수 있습니다.

class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }
  log(element) {
    console.log(this.prefix, element);
  }
}

const logger = new Logger("LOG:");
const array = ["a", "b", "c"];

array.forEach(logger.log); // TypeError: Cannot read properties of undefined (reading 'prefix')

array.forEach(logger.log, logger);
// 출력:
// LOG: a
// LOG: b
// LOG: c

// 아래와 거의 같은 의미를 가집니다
array.forEach(logger.log.bind(logger));

첫 번째 인수의 값이 화살표 함수인 경우, 두 번째 인수를 지정해도 의미가 없습니다.

화살표 함수식 내부의 this는 항상 화살표 함수식의 외부 스코프의 this와 일치하기 때문입니다.

배열의 forEach 메서드와 동일한 이름의 메서드가 다른 객체에도 존재할 수 있습니다.

예를 들어, MapSet에도 forEach 메서드가 있습니다.

해당 객체에 포함된 요소에 대해 반복 처리를 한다는 점에서 공통적이며, 콜백 함수의 인수가 배열의 것과 유사합니다.

배열의 forEach 메서드는 배열 유사 객체(array-like object)에 대해서도 반복 처리를 할 수 있습니다.

const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

Array.prototype.forEach.call(arrayLike, (element, i) => {
  console.log(i, element);
});
// 출력:
// 0 a
// 1 b
// 2 c

forEach 메서드의 변형

array가 배열이고, <처리>return 문이나 var, 함수 선언이 포함되지 않는 경우:

array.forEach((element) => {
  <처리>
});

는 다음과 거의 동일한 의미를 가집니다:

for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  <처리>
}

(ilength<처리> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열(dense array)일 경우 다음과도 거의 동일한 의미를 가집니다:

for (let element of array) {
  <처리>
}

(forEach 메서드 실행 중에 array의 길이가 증가하면 추가된 인덱스도 반복되는 차이점이 있습니다.)

<처리>return 문이 포함된 경우, 이 for 문에 대한 continue 문으로 대체해야 합니다.

map 메서드

배열의 map 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고 그 반환값을 모읍니다.

모든 요소에 대한 실행이 끝나면 map 메서드는 그 반환값을 모아 새로운 배열을 반환합니다.

원래 배열은 변경되지 않습니다.

const array = [1, 2, 3, 4, 5];

// 각 요소의 값을 두 배로 만든 새로운 배열을 생성
const doubled = array.map((element) => element * 2);
console.log(doubled); // 출력: [2, 4, 6, 8, 10]

// 원래 배열은 변경되지 않았음
console.log(array); // 출력: [1, 2, 3, 4, 5]

새로운 배열의 길이는 원래 배열의(처음의) 길이와 동일합니다.

콜백 함수의 반환값은 새로운 배열의 해당 인덱스 요소의 값이 됩니다.

따라서 forEach 메서드의 경우와 달리 반환값의 유무에 따라 의미가 달라집니다.

const array = [1, 2, 3];
const doubled = array.map((element) => element * 2);
console.log(doubled); // 출력: [2, 4, 6]

const array2 = array.map((element) => { element * 2; });
console.log(array2); // 출력: [undefined, undefined, undefined]

주의: => 뒤에 {가 이어지는 화살표 함수식에서는, 함수가 값을 반환하려면 return 문을 사용해야 합니다.

return 문 없이 함수 내부의 실행이(블록의 끝에 도달함으로써) 끝나면, 그 함수의 반환값은 암묵적으로 undefined가 됩니다.

map 메서드의 특징

배열의 map 메서드는 콜백 함수를 하나의 사상(寫像)으로 간주하고 배열의 각 요소 값을 그 사상에 따라 변환하는 것에 초점을 맞춘 메서드입니다.

따라서 일반적으로 콜백 함수는 반환값을 가지며, map 메서드의 반환값이 되는 배열을 사용해 후속 처리를 진행하는 경우가 많습니다.

배열의 map 메서드의 결과도 역시 배열이 되므로, map 메서드를 호출하는 식의 뒤에 또다시 map 메서드나 다른 배열 메서드를 연결할 수 있습니다.

이러한 메서드를 연결하는 형태를 메서드 체인이라고 부르기도 합니다.

const array = [1, 2, 3, 4, 5];

const doubledPlusOne = array
  .map((element) => element * 2) // 각 요소의 값을 두 배로 하고
  .map((element) => element + 1); // 한 번 더 더함

console.log(doubledPlusOne); // 출력: [3, 5, 7, 9, 11]

array
  .map((element) => element * 3) // 각 요소의 값을 세 배로 하고
  .filter((element) => element % 2 === 0) // 2로 나누어 떨어지는 것만 추출
  .forEach((element) => console.log(element)); // 각각을 출력
// 출력:
// 6
// 12

콜백 함수 내부에서 루프 전체를 중간에 종료하고 루프 바로 다음으로 처리 흐름을 이동시킬 수 있는 수단이 없다는 점은 forEach 메서드와 동일합니다.

현재 반복을 중간에 종료하고 다음 반복으로 넘어가기 위해서는(for 문에서의 continue 문에 해당) return 문을 사용해야 한다는 점도 forEach 메서드와 동일하지만, 그 return 문으로 반환하는 값은 map 메서드의 반환값이 되는 배열 요소의 값이 됩니다.

map 메서드만으로 결과 배열에서 요소를 제거하는 것은 불가능합니다.

const array = [1, 2, 3, 4, 5];

const newArray = array.map((element) => {
  if (element % 2 === 0) return; // 짝수는 무시하고 싶음
  return element * element;
});
// 결과 배열에 `undefined`가 포함됨
console.log(newArray); // 출력: [1, undefined, 9, undefined, 25]

요소를 제거하려면 filter 메서드를 추가로 사용해 불필요한 요소를 제외해야 합니다.

const newArray = array
  .map((element) => {
    if (element % 2 === 0) return; // 짝수는 무시하고 싶음
    return element * element;
  })
  .filter((element) => element !== undefined);
console.log(newArray); // 출력: [1, 9, 25]

map 메서드 대신 flatMap 메서드를 사용할 수도 있습니다.

const newArray = array.flatMap((element) => {
  if (element % 2 === 0) return []; // 짝수는 무시하고 싶음
  return [element * element];
});
console.log(newArray); // 출력: [1, 9, 25]

map 메서드에 전달하는 콜백 함수를 비동기 함수로 만들면, map 메서드의 반환값은 Promise로 구성된 배열이 됩니다.

const getDoubled = async (element) => {
  // 1초가 걸리는 비동기 처리를 재현
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return element * 2;
};

const array = [1, 2, 3];
const promises = array.map(async (element) => {
  const doubled = await getDoubled(element);
  return doubled;
});
console.log(promises);
// 출력: [Promise {<pending>}, Promise {<pending>}, Promise {<pending>}]

Promise.all을 사용하면 "Promise로 구성된 배열"을 "각 Promise의 해결값으로 구성된 배열로 해결되는 Promise"로 변환할 수 있습니다.

이것과 await 연산자를 조합하면 비동기 함수로 각 요소를 처리한 새로운 배열을 얻을 수 있습니다.

const array = [1, 2, 3];
const results = await Promise.all(
  array.map(async (element) => {
    const doubled = await getDoubled(element);
    return doubled;
  })
);
console.log(results); // 출력: [2, 4, 6]

이때 각 요소의 비동기 처리는 병렬로 실행된다는 점에 유의하세요.

전체 처리 시간은 가장 오래 걸리는 비동기 처리에 의해 결정됩니다. (위 예제에서는 각 요소의 비동기 처리가 약 1초 걸리므로 전체 처리 시간도 약 1초가 걸립니다.)

비동기 처리를 순차적으로 실행하려면 map 메서드 대신 for-of 문과 await 연산자를 사용해 반복 처리를 하거나 Promise 메서드를 사용해야 합니다.

const array = [1, 2, 3];
const results = [];
for (const element of array) {
  const doubled = await getDoubled(element);
  results.push(doubled);
}
console.log(results); // 출력: [2, 4, 6]
const array = [1, 2, 3];
const results = await array.reduce(
  (prev, element) => prev.then(async (results) => {
    const doubled = await getDoubled(element);
    results.push(doubled);
    return results;
  }),
  Promise.resolve([])
);
console.log(results); // 출력: [2, 4, 6]

순차 처리의 경우, 전체 처리 시간은 각 요소의 비동기 처리의 합계 시간이 됩니다.

(위 예제에서는 각 요소의 비동기 처리가 각각 약 1초씩 걸리므로 전체 처리 시간은 약 3초가 걸립니다.)

반복 대상이 되는 인덱스

map 메서드가 반복하는 범위와 반환값이 되는 새로운 배열의 길이는 메서드가 호출될 때의 원래 배열의 길이를 기준으로 결정됩니다.

따라서 map 메서드의 콜백 함수에서 배열의 길이를 늘린 경우, 원래 길이를 초과하는 인덱스의 요소는 반복되지 않으며 새로운 배열에도 포함되지 않습니다.

희소 배열의 프로퍼티가 없는(빈) 인덱스는 반복되지 않으며, 새로운 배열에서도 존재하지 않는(빈) 인덱스가 됩니다.

const array = ["a", , "c"];
const newArray = array.map((element, i) => element.repeat(i + 1));
console.log(newArray); // 출력: [ 'a', <1 empty item>, 'ccc' ]

콜백 함수의 세 번째 인수

콜백 함수의 세 번째 인수는 메서드 체인에서 map 메서드를 호출할 때 유용할 수 있습니다.(forEach와 마찬가지로)

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const twinPrimes = [];
array
  .filter((element) => isPrime(element)) // 소수만 추출
  .map((element, i, primes) => {
    // 세 번째 인수 primes의 값은 반복 대상인 배열, 즉 소수만 추출된 배열(array가 아닌 array.filter(...))

    if (i === 0) return; // 첫 번째 요소는 바로 앞의 소수가 없으므로 건너뜀
    const prev = primes[i - 1]; // 바로 앞의 소수를 primes에서 가져옴
    // 쌍둥이 소수인지 판별
    if (element - prev === 2) {
      twinPrimes.push([prev, element]);
    }
  });

console.log(twinPrimes);
// 출력: [ [ 3, 5 ], [ 5, 7 ] ]

콜백 함수에는 항상 3개의 인수가 전달되므로 생략 가능한 인수를 받는 함수의 취급에 주의해야 합니다.

특히, parseInt 함수를 그대로 콜백 함수로 사용하면 의도하지 않은 결과가 나올 수 있습니다.

const array = ["10", "10", "10", "10"];

// 10진수 정수로 해석한 숫자 배열로 변환하고 싶지만……
const newArray = array.map(parseInt);
console.log(newArray); // 출력: [10, NaN, 2, 3]

// `parseInt` 함수는 생략 가능한 두 번째 인수가 지정된 경우 그 값을 기수로 사용해 첫 번째 인수의 문자열을 해석합니다.
// 한편, `map` 메서드는 콜백 함수의 두 번째 인수로 인덱스를 전달합니다.
parseInt("10", 0, array) // → 10 (자동으로 10진수로 해석됨)
parseInt("10", 1, array) // → NaN (1은 기수로 무효)
parseInt("10", 2, array) // → 2 (2진수로 해석됨)
parseInt("10", 3, array) // → 3 (3진수로 해석됨)

// 의도한 대로 동작하는 예: 두 번째 인수 이후는 `parseInt`에 전달하지 않음
const newArray2 = array.map((element) => parseInt(element));
console.log(newArray2); // 출력: [10, 10, 10, 10]

map 메서드의 두 번째 인수

map 메서드의 두 번째 인수로 콜백 함수에서 this로 사용될 값을 지정할 수 있습니다. (forEach와 마찬가지로)

class Logger {
  constructor(prefix) {
    this.prefix = prefix;
  }
  log(element) {
    console.log(this.prefix, element);
  }
}

const logger = new Logger("LOG:");
const array = ["a", "b", "c"];

const newArray = array.map(logger.log, logger);
console.log(newArray); // 출력: [undefined, undefined, undefined]

배열의 map 메서드는 배열 유사 객체에도 호출할 수 있으며, 반환값은 배열이 됩니다.

const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const newArray = Array.prototype.map.call(arrayLike, (element, i) => element.repeat(i + 1));
console.log(newArray); // 출력: [ 'a', 'bb', 'ccc' ]

map 메서드의 변형

array가 배열일 때:

const newArray = array.map((element) => {
  const newElement = <식>;
  return newElement;
});

<식>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

const newArray = new Array(+array.length);
for (let i = 0; i < newArray.length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  const newElement = <식>;
  newArray[i] = newElement;
}

(i<식> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

const newArray = [];
for (let element of array) {
  const newElement = <식>;
  newArray.push(newElement);
}

와도 거의 동일한 의미를 가집니다. (map 메서드 실행 중에 array의 길이가 증가할 때 추가된 인덱스도 반복된다는 차이점이 있습니다.)

map 메서드를 여러 번 연결하는 패턴의 변형

array가 배열일 때:

const newArray = array
  .map((element) => <element를 사용하는 식 1>)
  .map((element) => <element를 사용하는 식 2>)
  .map((element) => <element를 사용하는 식 3>);

<element를 사용하는 식 1>, <element를 사용하는 식 2>, <element를 사용하는 식 3>에 부작용이 없다면 다음과 같이 변형할 수 있습니다.

const newArray = array.map((element) => {
  const newElement1 = <element를 사용하는 식 1>;
  const newElement2 = <newElement1을 사용하는 식 2>;
  const newElement3 = <newElement2를 사용하는 식 3>;
  return newElement3;
});

(newElement1, newElement2, newElement3은 3개의 식에서 참조되지 않는 변수명이어야 합니다.)

map 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행한 후 배열을 반환하므로, map 메서드를 여러 번 연결한 경우:

  1. 모든 요소에 대해 순서대로 식 1이 평가됨
  2. 모든 요소에 대해 순서대로 식 2가 평가됨
  3. 모든 요소에 대해 순서대로 식 3이 평가됨

이라는 순서로 처리됩니다. 한편, 하나의 map 메서드에서 3개의 식을 모두 실행하는 경우:

  1. 첫 번째 요소에 대해 식 1, 식 2, 식 3이 순서대로 평가됨
  2. 두 번째 요소에 대해 식 1, 식 2, 식 3이 순서대로 평가됨
  3. 마지막 요소에 대해 식 1, 식 2, 식 3이 순서대로 평가됨

라는 순서로 처리됩니다.

따라서 부작용이 있는 식을 사용하는 경우, 이러한 변형이 의미를 바꿀 수 있습니다.

const array = ["a", "b", "c"];

const newArray = array
  .map((element) => {
    // 부작용: console.log를 통한 출력
    console.log("1:", element);
    return element.toUpperCase();
  })
  .map((element) => {
    // 부작용: console.log를 통한 출력
    console.log("2:", element);
    return element.repeat(2);
  });
console.log(newArray);
// 출력:
// 1: a
// 1: b
// 1: c
// 2: A
// 2: B
// 2: C
// [ 'AA', 'BB', 'CC' ]
const array = ["a", "b", "c"];

const newArray = array.map((element) => {
  console.log("1:", element);
  const newElement1 = element.toUpperCase();
  console.log("2:", newElement1);
  const newElement2 = newElement1.repeat(2);
  return newElement2;
});
console.log(newArray);
// 출력: (최종 배열은 동일하지만, 중간 출력의 순서가 다름)
// 1: a
// 2: A
// 1: b
// 2: B
// 1: c
// 2: C
// [ 'AA', 'BB', 'CC' ]

map 메서드 다음에 flat 메서드를 사용하는 경우

array가 배열일 때:

const newArray = array
  .map((element) => <식>)
  .flat();

는(.flat(1)도 포함하여):

const newArray = array.flatMap((element) => <식>);

과 거의 동일한 의미를 가집니다.

map 메서드의 반환값을 사용하지 않는 경우

배열의 map 메서드의 반환값을 사용하지 않는 경우 대신 forEach 메서드를 사용할 수 있습니다.

array.map((element) => {
  <처리>
});

array.forEach((element) => {
  <처리>
});

와 거의 동일한 의미를 가집니다.

filter 메서드

배열의 filter 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과값이 true인 요소를 모아 새로운 배열을 반환합니다.

모든 요소에 대한 실행이 끝나면 filter 메서드는 그 결과값이 true인 요소를 모아 새로운 배열을 반환합니다. 원래 배열은 변경되지 않습니다.

const array = [1, 2, 3, 4, 5];

// 각 요소의 값이 짝수인 요소만 모아 새로운 배열을 생성
const evens = array.filter((element) => element % 2 === 0);
console.log(evens); // 출력: [2, 4]

// 원래 배열은 변경되지 않음
console.log(array); // 출력: [1, 2, 3, 4, 5]

filter 메서드의 특징

배열의 filter 메서드는 콜백 함수의 반환값이 true인 경우 해당 요소를 결과 배열에 포함시키는 데 중점을 둔 메서드입니다.

따라서 일반적으로 콜백 함수는 반환값을 가지며, filter 메서드의 반환값이 되는 배열을 사용해 후속 처리를 진행하는 경우가 많습니다.

const array = [1, 2, 3, 4, 5];

const evenSquares = array
  .filter((element) => element % 2 === 0) // 짝수만 추출
  .map((element) => element * element); // 제곱하여 새로운 배열 생성

console.log(evenSquares); // 출력: [4, 16]

filter 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행한 후 배열을 반환하므로, filter 메서드를 여러 번 연결한 경우:

  1. 모든 요소에 대해 순서대로 조건 1이 평가됨
  2. 모든 요소에 대해 순서대로 조건 2가 평가됨
  3. 모든 요소에 대해 순서대로 조건 3이 평가됨

이라는 순서로 처리됩니다. 한편, 하나의 filter 메서드에서 여러 조건을 모두 실행하는 경우:

  1. 첫 번째 요소에 대해 조건 1, 조건 2, 조건 3이 순서대로 평가됨
  2. 두 번째 요소에 대해 조건 1, 조건 2, 조건 3이 순서대로 평가됨
  3. 마지막 요소에 대해 조건 1, 조건 2, 조건 3이 순서대로 평가됨

이라는 순서로 처리됩니다. 따라서 부작용이 있는 조건을 사용하는 경우, 이러한 변형이 의미를 바꿀 수 있습니다.

const array = [1, 2, 3, 4, 5, 6];

const evenAndGreaterThanThree = array
  .filter((element) => {
    console.log("조건 1:", element);
    return element % 2 === 0;
  })
  .filter((element) => {
    console.log("조건 2:", element);
    return element > 3;
  });
console.log(evenAndGreaterThanThree); 
// 출력:
// 조건 1: 1
// 조건 1: 2
// 조건 1: 3
// 조건 1: 4
// 조건 1: 5
// 조건 1: 6
// 조건 2: 4
// 조건 2: 6
// [ 4, 6 ]
const array = [1, 2, 3, 4, 5, 6];

const evenAndGreaterThanThree = array.filter((element) => {
  console.log("조건 1:", element);
  if (element % 2 !== 0) return false;
  console.log("조건 2:", element);
  return element > 3;
});
console.log(evenAndGreaterThanThree);
// 출력:
// 조건 1: 1
// 조건 1: 2
// 조건 1: 3
// 조건 1: 4
// 조건 1: 5
// 조건 1: 6
// 조건 2: 4
// 조건 2: 6
// [ 4, 6 ]

filter 메서드의 변형

array가 배열일 때:

const newArray = array.filter((element) => <조건>);

<조건>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

const newArray = [];
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (!<조건>) continue;
  newArray.push(element);
}

(i, length<조건> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

const newArray = [];
for (let element of array) {
  if (!<조건>) continue;
  newArray.push(element);
}

와도 거의 동일한 의미를 가집니다.

filter 메서드와 forEach 메서드의 조합

filter 메서드로 걸러내고 forEach 메서드로 처리하는 경우:

array
  .filter((element) => <조건>)
  .forEach((element) => {
    <처리>
  });

는 다음과 같은 의미를 가집니다.

for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (!<조건>) continue;
  <처리>
}

또는

for (let element of array) {
  if (!<조건>) continue;
  <처리>
}

filter 메서드의 반환값을 사용하지 않는 경우

배열의 filter 메서드의 반환값을 사용하지 않는 경우 대신 forEach 메서드를 사용할 수 있습니다.

array.filter((element) => {
  if (!<조건>) return false;
  <처리>
});

는 다음과 동일한 의미를 가집니다.

array.forEach((element) => {
  if (!<조건>) return;
  <처리>
});

reduce 메서드

reduce 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과를 누적하여 최종 결과를 반환합니다.

모든 요소에 대한 실행이 끝나면 reduce 메서드는 최종 결과를 반환합니다. 원래 배열은 변경되지 않습니다.

const array = [1, 2, 3, 4, 5];

// 배열의 모든 요소를 더한 값을 반환
const sum = array.reduce((accumulator, element) => accumulator + element, 0);
console.log(sum); // 출력: 15

reduce 메서드의 특징

reduce 메서드의 콜백 함수는 누적된 값(accumulator)과 현재 요소(element), 인덱스(index), 그리고 배열 전체(array)를 인수로 받습니다.

reduce 메서드의 초기값

reduce 메서드는 누적된 값의 초기값을 두 번째 인수로 받습니다. 생략한 경우 첫 번째 요소가 초기값이 되며, 두 번째 요소부터 반복이 시작됩니다.

const array = [1, 2, 3, 4, 5];

// 초기값을 지정하지 않은 경우
const sum = array.reduce((accumulator, element) => accumulator + element);
console.log(sum); // 출력: 15

// 초기값을 지정한 경우
const sumWithInitial = array.reduce((accumulator, element) => accumulator + element, 10);
console.log(sumWithInitial); // 출력: 25

reduce 메서드의 변형

array가 배열일 때:

const result = array.reduce((accumulator, element) => {
  <처리>
  return accumulator;
}, <초기값>);

<처리>accumulatorarray의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let accumulator = <초기값>;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  <처리>
}

(i, length<처리> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let accumulator = <초기값>;
for (let element of array) {
  <처리>
}

와도 거의 동일한 의미를 가집니다.

reduce 메서드로 객체를 구성하는 예제

const people = [
  { name: "Alice", age: 20 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 30 },
];

const peopleByName = people.reduce((acc, person) => {
  acc[person.name] = person;
  return acc;
}, {});
console.log(peopleByName);
// 출력: { Alice: { name: "Alice", age: 20 }, Bob: { name: "Bob", age: 25 }, Charlie: { name: "Charlie", age: 30 } }

reduce 메서드로 중첩된 배열을 평탄화하는 예제

const nestedArray = [[1, 2], [3, 4], [5, 6]];

const flattenedArray = nestedArray.reduce((acc, array) => acc.concat(array), []);
console.log(flattenedArray); // 출력: [1, 2, 3, 4, 5, 6]

reduce 메서드로 객체 배열에서 특정 프로퍼티의 합계를 구하는 예제

const orders = [
  { id: 1, amount: 250 },
  { id: 2, amount: 400 },
  { id: 3, amount: 100 },
  { id: 4, amount: 325 }
];

const totalAmount = orders.reduce((acc, order) => acc + order.amount, 0);
console.log(totalAmount); // 출력: 1075

reduce 메서드로 배열을 그룹화하는 예제

const people = [
  { name: "Alice", gender: "female" },
  { name: "Bob", gender: "male" },
  { name: "Eve", gender: "female" },
  { name: "David", gender: "male" },
];

const groupedByGender = people.reduce((acc, person) => {
  const key = person.gender;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(person);
  return acc;
}, {});
console.log(groupedByGender);
// 출력:
// {
//   female: [{ name: "Alice", gender: "female" }, { name: "Eve", gender: "female" }],
//   male: [{ name: "Bob", gender: "male" }, { name: "David", gender: "male" }]
// }

reduce 메서드로 배열의 중복 요소를 제거하는 예제

const array = [1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 9];

const uniqueArray = array.reduce((acc, element) => {
  if (!acc.includes(element)) {
    acc.push(element);
  }
  return acc;
}, []);
console.log(uniqueArray); // 출력: [1, 2, 3, 4, 5, 6, 7, 8, 9]

reduce 메서드로 배열의 최대값을 구하는 예제

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];

const maxValue = array.reduce((max, element) => (element > max ? element : max), array[0]);
console.log(maxValue); // 출력: 9

reduce 메서드를 사용하여 mapfilter를 구현하는 예제

map 메서드처럼 동작하는 mapWithReduce 함수:

const mapWithReduce = (array, callback) => {
  return array.reduce((acc, element, index, array) => {
    acc.push(callback(element, index, array));
    return acc;
  }, []);
};

const array = [1, 2, 3, 4, 5];
const doubled = mapWithReduce(array, (element) => element * 2);
console.log(doubled); // 출력: [2, 4, 6, 8, 10]

filter 메서드처럼 동작하는 filterWithReduce 함수:

const filterWithReduce = (array, callback) => {
  return array.reduce((acc, element, index, array) => {
    if (callback(element, index, array)) {
      acc.push(element);
    }
    return acc;
  }, []);
};

const evens = filterWithReduce(array, (element) => element % 2 === 0);
console.log(evens); // 출력: [2, 4]

reduce 메서드의 변형

array가 배열일 때:

const result = array.reduce((acc, element) => {
  <처리>
  return acc;
}, <초기값>);

<처리>accarray의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let acc = <초기값>;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  <처리>
}

(i, length<처리> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let acc = <초기값>;
for (let element of array) {
  <처리>
}

와도 거의 동일한 의미를 가집니다.

find 메서드

find 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과값이 true인 첫 번째 요소를 반환합니다.

모든 요소에 대한 실행이 끝나거나 true인 요소를 찾으면 find 메서드는 그 첫 번째 요소를 반환하고, 그렇지 않으면 undefined를 반환합니다.

const array = [1, 2, 3, 4, 5];

// 배열에서 첫 번째로 짝수인 요소를 찾음
const firstEven = array.find((element) => element % 2 === 0);
console.log(firstEven); // 출력: 2

find 메서드의 특징

find 메서드는 첫 번째로 조건에 맞는 요소를 찾는 데 주안점을 둔 메서드입니다.

따라서 일반적으로 콜백 함수는 조건에 맞는 요소를 찾을 때 반환값을 true로 설정하며, find 메서드의 반환값이 되는 요소를 사용해 후속 처리를 진행하는 경우가 많습니다.

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" },
];

// ID가 2인 첫 번째 사용자를 찾음
const user = users.find((user) => user.id === 2);
console.log(user); // 출력: { id: 2, name: "Bob" }

find 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행하므로, 첫 번째로 조건에 맞는 요소를 찾은 경우 콜백 함수의 실행이 즉시 중단됩니다.

find 메서드의 변형

array가 배열일 때:

const result = array.find((element) => <조건>);

<조건>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let result;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (<조건>) {
    result = element;
    break;
  }
}

(i, length<조건> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let result;
for (let element of array) {
  if (<조건>) {
    result = element;
    break;
  }
}

와도 거의 동일한 의미를 가집니다.

findIndex 메서드

findIndex 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과값이 true인 첫 번째 요소의 인덱스를 반환합니다.

모든 요소에 대한 실행이 끝나거나 true인 요소를 찾으면 findIndex 메서드는 그 첫 번째 요소의 인덱스를 반환하고, 그렇지 않으면 -1을 반환합니다.

const array = [1, 2, 3, 4, 5];

// 배열에서 첫 번째로 짝수인 요소의 인덱스를 찾음
const firstEvenIndex = array.findIndex((element) => element % 2 === 0);
console.log(firstEvenIndex); // 출력: 1

findIndex 메서드의 특징

findIndex 메서드는 첫 번째로 조건에 맞는 요소의 인덱스를 찾는 데 주안점을 둔 메서드입니다.

따라서 일반적으로 콜백 함수는 조건에 맞는 요소를 찾을 때 반환값을 true로 설정하며, findIndex 메서드의 반환값이 되는 인덱스를 사용해 후속 처리를 진행하는 경우가 많습니다.

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" }
];

// ID가 2인 첫 번째 사용자의 인덱스를 찾음
const userIndex = users.findIndex((user) => user.id === 2);
console.log(userIndex); // 출력: 1

findIndex 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행하므로, 첫 번째로 조건에 맞는 요소를 찾은 경우 콜백 함수의 실행이 즉시 중단됩니다.

findIndex 메서드의 변형

array가 배열일 때:

const index = array.findIndex((element) => <조건>);

<조건>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let index = -1;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (<조건>) {
    index = i;
    break;
  }
}

(i, length<조건> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let index = -1;
for (let [i, element] of array.entries()) {
  if (<조건>) {
    index = i;
    break;
  }
}

와도 거의 동일한 의미를 가집니다.

some 메서드

some 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과값이 true인 요소가 하나라도 있으면 true를 반환합니다.

모든 요소에 대한 실행이 끝나거나 true인 요소를 찾으면 some 메서드는 true를 반환하고, 그렇지 않으면 false를 반환합니다.

const array = [1, 2, 3, 4, 5];

// 배열에 짝수가 하나라도 있는지 확인
const hasEven = array.some((element) => element % 2 === 0);
console.log(hasEven); // 출력: true

some 메서드의 특징

some 메서드는 배열에 하나라도 조건에 맞는 요소가 있는지 확인하는 데 주안점을 둔 메서드입니다.

따라서 일반적으로 콜백 함수는 조건에 맞는 요소를 찾을 때 반환값을 true로 설정하며, some 메서드의 반환값을 사용해 후속 처리를 진행하는 경우가 많습니다.

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" }
];

// 이름이 "Alice"인 사용자가 하나라도 있는지 확인
const hasAlice = users.some((user) => user.name === "Alice");
console.log(hasAlice); // 출력: true

some 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행하므로, 첫 번째로 조건에 맞는 요소를 찾은 경우 콜백 함수의 실행이 즉시 중단됩니다.

some 메서드의 변형

array가 배열일 때:

const result = array.some((element) => <조건>);

<조건>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let result = false;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (<조건>) {
    result = true;
    break;
  }
}

(i, length<조건> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let result = false;
for (let element of array) {
  if (<조건>) {
    result = true;
    break;
  }
}

와도 거의 동일한 의미를 가집니다.

every 메서드

every 메서드는 배열의 각 요소에 대해 순서대로 콜백 함수를 실행하고, 그 결과값이 true인 요소가 전부일 때만 true를 반환합니다.

모든 요소에 대한 실행이 끝나거나 false인 요소를 찾으면 every 메서드는 false를 반환합니다.

const array = [1, 2, 3, 4, 5];

// 배열의 모든 요소가 1보다 큰지 확인
const allGreaterThanOne = array.every((element) => element > 1);
console.log(allGreaterThanOne); // 출력: false

every 메서드의 특징

every 메서드는 배열의 모든 요소가 조건에 맞는지 확인하는 데 주안점을 둔 메서드입니다.

일반적으로 콜백 함수는 조건에 맞는 요소일 때 반환값을 true로 설정하며, every 메서드의 반환값을 사용해 후속 처리를 진행하는 경우가 많습니다.

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" }
];

// 모든 사용자가 18세 이상인지 확인
const allAdults = users.every((user) => user.age >= 18);
console.log(allAdults); // 출력: true

every 메서드는 모든 요소에 대해 콜백 함수를 순서대로 실행하므로, 첫 번째로 조건에 맞지 않는 요소를 찾은 경우 콜백 함수의 실행이 즉시 중단됩니다.

every 메서드의 변형

array가 배열일 때:

const result = array.every((element) => <조건>);

<조건>array의 값을 변경하지 않는다면 다음과 거의 동일한 의미를 가집니다.

let result = true;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  if (!<조건>) {
    result = false;
    break;
  }
}

(i, length<조건> 내부에서 참조되지 않는 변수명이어야 합니다.)

또한, array가 밀집 배열일 경우:

let result = true;
for (let element of array) {
  if (!<조건>) {
    result = false;
    break;
  }
}

와도 거의 동일한 의미를 가집니다.


find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드

배열의 find 메서드는 배열의 각 요소에 대해 순차적으로 콜백 함수를 실행하고, 그 반환값이 truthy하면 그 지점에서 반복을 종료하고 해당 요소의 값을 반환합니다.

truthy한 반환값이 나오지 않으면 반복이 종료되어 undefined를 반환합니다.

const array = [1, 2, 3, 4, 5];

const firstEven = array.find((element) => {
  console.log("is even?", element);
  return element % 2 === 0;
});
console.log(firstEven);
// 출력:
// is even? 1
// is even? 2
// 2

const firstNegative = array.find((element) => {
  console.log("is negative?", element);
  return element < 0;
});
console.log(firstNegative);
// 출력:
// is negative? 1
// is negative? 2
// is negative? 3
// is negative? 4
// is negative? 5
// undefined

배열의 findIndex 메서드는 find 메서드와 유사하지만, 콜백 함수가 truthy한 값을 반환한 요소의 값 대신 그 요소의 인덱스를 반환합니다.

truthy한 반환값이 나오지 않으면 반복이 종료되어 -1을 반환합니다.

const array = [1, 2, 3, 4, 5];

const firstEvenIndex = array.findIndex((element) => {
  console.log("is even?", element);
  return element % 2 === 0;
});
console.log(firstEvenIndex);
// 출력:
// is even? 1
// is even? 2
// 1

const firstNegativeIndex = array.findIndex((element) => {
  console.log("is negative?", element);
  return element < 0;
});
console.log(firstNegativeIndex);
// 출력:
// is negative? 1
// is negative? 2
// is negative? 3
// is negative? 4
// is negative? 5
// -1

특히, 원래 배열이 빈 배열일 경우 콜백 함수는 호출되지 않으며, find 메서드는 항상 undefined를, findIndex 메서드는 항상 -1을 반환합니다.

findLast 메서드와 findLastIndex 메서드는 각각 find 메서드와 findIndex 메서드와 유사하지만, 배열의 끝에서 시작하여 앞으로 향해 반복한다는 점이 다릅니다.

const array = [1, 2, 3, 4, 5];

const lastEven = array.findLast((element) => {
  console.log("is even?", element);
  return element % 2 === 0;
});
console.log(lastEven);
// 출력:
// is even? 5
// is even? 4
// 4

const lastNegative = array.findLast((element) => {
  console.log("is negative?", element);
  return element < 0;
});
console.log(lastNegative);
// 출력:
// is negative? 5
// is negative? 4
// is negative? 3
// is negative? 2
// is negative? 1
// undefined

find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드의 특징

배열의 find 메서드와 findLast 메서드는 특정 조건을 만족하는 요소를 하나만 골라 그 요소의 값을 얻는 작업을 수행합니다. 그 조건은 콜백 함수를 사용해 자유롭게 지정할 수 있습니다.

find 메서드나 findLast 메서드는 배열의 요소 값뿐만 아니라 undefined를 반환할 가능성도 있습니다.

find 메서드나 findLast 메서드가 undefined를 반환하면 두 가지 가능성이 있습니다.

  1. 원래 배열에 조건을 만족하는 요소가 하나도 없었던 경우
  2. 배열의 요소 값으로 undefined가 존재하며, 해당 요소가 조건을 만족한 경우

여기서 "요소"에는 프로퍼티가 존재하지 않는(빈) 인덱스도 포함됩니다. 콜백 함수에서 지정한 조건을 undefined가 반드시 충족하지 않는다면, 후자의 가능성을 배제할 수 있으며, 원래 배열에 조건을 만족하는 요소가 없었다는 것을 의미합니다.

배열의 findIndex 메서드와 findLastIndex 메서드는 특정 조건을 만족하는 요소를 하나만 골라 그 요소의 인덱스를 얻는 작업을 수행합니다.

그 조건은 콜백 함수를 사용해 자유롭게 지정할 수 있습니다.

findIndex 메서드나 findLastIndex 메서드가 -1을 반환하면, 원래 배열에 조건을 만족하는 요소가 하나도 없었다는 것을 의미합니다.

콜백 함수가 truthy한 값을 반환하면 그 시점에서 반복이 종료되고, 해당 요소의 값 또는 인덱스가 반환됩니다.

이 동작은 for 문에서의 break 문 또는 return 문에 해당합니다.

이를 통해 긴 배열이라도 앞부분에 조건을 충족하는 요소가 있다면 나머지 요소에 대한 반복을 생략하여 효율적으로 요소를 찾을 수 있습니다.

현재 반복을 중단하고 다음 반복으로 넘어가려면(for 문에서의 continue 문에 해당) return 문으로 falsy한 값을 반환합니다.

조건을 만족하는 요소의 값 또는 인덱스에 관심이 없고, 조건을 만족하는 요소가 존재하는지만 알고 싶다면 some 메서드나 every 메서드를 사용할 수 있습니다.

반복 대상 인덱스

find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드에 전달하는 콜백 함수에서 비동기로 진리값을 반환할 수 없습니다.

이유는 filter 메서드와 동일합니다. 대신 for 문과 await 연산자를 사용한 순차 반복 처리를 할 수 있습니다.

콜백 함수의 세 번째 인자는 이들 메서드를 메서드 체인에서 호출할 때 유용할 수 있습니다(forEach와 유사).

find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드의 두 번째 인자로 콜백 함수 내에서 this로 사용될 값을 지정할 수 있습니다(forEach와 유사).

배열의 find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드는 배열과 유사한 객체에서도 호출할 수 있습니다.

find 메서드, findIndex 메서드, findLast 메서드, findLastIndex 메서드의 대체 구현

array가 배열일 때,

let result = array.find((element) => {
  const condition = <조건>;
  return condition;
});

<조건>array 값을 변경하지 않는다면

let result = undefined;
for (let i = 0, length = +array.length; i < length; i++) {
  let element = array[i];
  const condition = <조건>;
  if (condition) {
    result = element;
    break;
  }
}

와 거의 같은 의미가 됩니다(ilength<조건> 안에서 참조되지 않는 변수명으로 해야 함).

또한,

let result = undefined;
for (let element of array) {
  const condition = <조건>;
  if (condition) {
    result = element;
    break;
  }
}

와도 거의 같은 의미가 됩니다(find 메서드 실행 중에 array의 길이가 늘어난 경우 추가된 인덱스도 반복된다는 차이점이 있음).

array가 배열일 때,

let result = array.findIndex((element) => {
  const condition = <조건>;
  return condition;
});

<조건>array 값을 변경하지 않는다면

let result = -1;
for (let i = 0, length = +array.length; i < length; i++) {
  let element = array[i];
  const condition = <조건>;
  if (condition) {
    result = i;
    break;
  }
}

와 거의 같은 의미가 됩니다(ilength<조건> 안에서 참조되지 않는 변수명으로 해야 함).

또한,

let result = -1;
for (let [i, element] of array.entries()) {
  const condition = <조건>;
  if (condition) {
    result = i;
    break;
  }
}

와도 거의 같은 의미가 됩니다(findIndex 메서드 실행 중에 array의 길이가 늘어난 경우 추가된 인덱스도 반복된다는 차이점이 있음).

array가 배열일 때,

let result = array.findLast((element) => {
  const condition = <조건>;
  return condition;
});

는 비슷하게

let result = undefined;
for (let i = array.length - 1; i >= 0; i--) {
  let element = array[i];
  const condition = <조건>;
  if (condition) {
    result = element;
    break;
  }
}

와 거의 같은 의미가 되며,

let result = array.findLastIndex((element) => {
  const condition = <조건>;
  return condition;
});

let result = -1;
for (let i = array.length - 1; i >= 0; i--) {
  let element = array[i];
  const condition = <조건>;
  if (condition) {
    result = i;
    break;
  }
}

와 거의 같은 의미가 됩니다(i<조건> 안에서 참조되지 않는 변수명으로 해야 함).

특정 값을 찾는 패턴의 대체 구현

빽빽한 배열 array에서 특정 값을 가진 요소의 인덱스를 찾을 때,

const search = <값>;
const index = array.findIndex((element) => element === search);

const search = <값>;
const index = array.indexOf(search);

// 또는
const index = array.indexOf(<값>);

와 거의 같은 의미가 되며,

const search = <값>;
const index = array.findLastIndex((element) => element === search);

const search = <값>;
const index = array.lastIndexOf(search);

// 또는
const index = array.lastIndexOf(<값>);

와 거의 같은 의미가 됩니다.

findIndex 메서드나 findLastIndex 메서드를 사용할 때 <값>이 배열의 각 요소에 대해 반복적으로 평가되는 것을 피하기 위해 위와 같은 코드 예제에서처럼 콜백 함수 밖에서 변수 search에 할당하는 경우가 있지만, indexOf 메서드나 lastIndexOf 메서드를 사용하면 <값>을 직접 인자로 전달할 수 있습니다.


flatMap 메서드

배열의 flatMap 메서드는 배열의 각 요소에 대해 순차적으로 콜백 함수를 실행하여, 반환값이 배열이라면 해당 배열의 요소를, 배열이 아닌 값이라면 그 값을 그대로 수집합니다.

모든 요소에 대해 실행이 완료되면 flatMap 메서드는 수집한 값들로 구성된 새로운 배열을 반환합니다. 원래 배열은 변경되지 않습니다.

const array = [1, 2, 3];

// 각 요소의 값을 그 숫자만큼 반복
const repeated = array.flatMap((element) => new Array(element).fill(element));
console.log(repeated); // 출력: [ 1, 2, 2, 3, 3, 3 ]

// 원래 배열은 변경되지 않음
console.log(array); // 출력: [ 1, 2, 3 ]

flatMap 메서드의 특징

배열의 flatMap 메서드는 map 메서드와 flat 메서드를 결합한 작업을 수행합니다. flat 메서드는 2차원 배열을 1차원 배열로 평탄화하는 작업을 수행합니다.

const array = [1, 2, 3];

const array2 = array.map((element) => new Array(element).fill(element)); // [ [ 1 ], [ 2, 2 ], [ 3, 3, 3 ] ]
const flat = array2.flat(); // [ 1, 2, 2, 3, 3, 3 ] (`flatMap` 메서드의 결과와 동일)

배열의 map 메서드에서는 원래 배열과 결과 배열의 길이가 같으며, 요소가 1:1로 대응됩니다. 반면에 flatMap 메서드에서는 원래 배열의 한 요소에 대해 결과 배열에 여러 요소를 생성하거나, 결과 배열에 요소를 포함하지 않는(0개의 요소를 생성하는) 경우도 가능합니다.

const books = [
  { title: "A", author: "X", tags: ["a", "b"] },
  { title: "B", author: "Y", tags: ["b", "c"] },
  { title: "C", author: "Z", tags: ["c", "d"] },
];

// 태그 "b"를 포함하는 각 책에 대해 제목과 저자를 생성
const result = books.flatMap((book) => {
  if (!book.tags.includes("b")) {
    return []; // 빈 배열 반환(0개의 요소 생성)
  }
  // 길이 2의 배열 반환(2개의 요소 생성)
  return [
    book.title,
    `by ${book.author}`,
  ];
});
console.log(result); // 출력: [ 'A', 'by X', 'B', 'by Y' ]

배열이 아닌 값의 처리

콜백 함수의 반환값이 배열이 아닌 배열과 유사한 객체인 경우, 해당 배열과 유사한 객체는 평탄화되지 않고 결과 배열의 한 요소로 포함됩니다.

평탄화의 깊이

flatMap 메서드는 기본적으로 한 단계만 평탄화합니다. 더 깊은 평탄화가 필요하다면 flat 메서드를 조합하는 것이 좋습니다.

반복 처리

콜백 함수 내에서 반복 전체를 중단하고 바로 반복 이후로 이동하는 수단이 없다는 점은 forEach 메서드와 유사합니다.

비동기 콜백 함수

flatMap 메서드의 콜백 함수에 비동기 처리를 포함할 수는 없지만, for 문과 await 연산자를 사용해 순차 반복 처리를 할 수 있습니다.

메서드 체인에서 유용한 세 번째 인자

콜백 함수의 세 번째 인자는 메서드 체인에서 flatMap 메서드를 호출할 때 유용할 수 있습니다(forEach처럼).

flatMap 메서드의 this 지정

flatMap 메서드의 두 번째 인자는 콜백 함수 내에서 this로 사용될 값을 지정할 수 있습니다(forEach와 유사).

배열과 유사한 객체에 대한 호출

배열의 flatMap 메서드는 배열과 유사한 객체에서도 호출할 수 있습니다. 반환값은 배열입니다.

flatMap 메서드의 대체 구현

array가 배열일 때,

const newArray = array.flatMap((element) => {
  const result = <식>;
  return result;
});

<식>이 배열의 값을 변경하지 않는다면

const newArray = [];
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let element = array[i];
  const result = <식>;
  newArray.push(...[result].flat());
}

와 거의 같은 의미가 됩니다(ilength<식> 안에서 참조되지 않는 변수명으로 해야 함).

또한, array가 빽빽한 배열일 때,

const newArray = [];
for (let element of array) {
  const result = <식>;
  newArray.push(...[result].flat());
}

와도 거의 같은 의미가 됩니다(flatMap 메서드 실행 중에 array의 길이가 늘어나면 추가된 인덱스도 반복된다는 차이점이 있음).

특히 <식>의 값이 반드시 빽빽한 배열이 되는 경우

newArray.push(...[result].flat());

부분은

newArray.push(...result);

로 바꿀 수 있습니다.

array가 빽빽한 배열이고 <식>의 값이 반드시 배열이 아닌 값이 되는 경우 flatMap 메서드 대신 map 메서드를 사용해도 대부분의 경우 동일한 결과를 얻을 수 있습니다.

array가 배열일 때, 콜백 함수가 항등 함수라면

const newArray = array.flatMap((element) => element);

const newArray = array.flat();

로 바꿀 수 있습니다.

reduce 메서드, reduceRight 메서드

배열의 reduce 메서드는 지금까지 소개한 메서드와 달리 콜백 함수에 4개의 인자를 전달합니다.

  1. 누적값: 이전 반복에서의 계산 결과
  2. 현재 요소의 값: 현재 반복에서 대상이 되는 요소의 값
  3. 현재 인덱스: 현재 반복에서 대상이 되는 요소의 인덱스
  4. 배열: 반복 대상인 배열 전체

또한 reduce 메서드의 두 번째 인자로 누적값의 초기값을 전달할 수 있습니다.

reduce 메서드는 배열의 각 요소에 대해 순차적으로 콜백 함수를 실행합니다.

콜백 함수의 반환값은 다음 반복에서 콜백 함수에 전달되는 누적값이 됩니다.

마지막 반복이 끝나면 reduce 메서드의 반환값이 됩니다.

const array = ["Alice", "Bob", "Charlie"];

/** 배열의 각 요소 값인 문자열 길이의 합을 구함 */
const totalLength = array.reduce(
  // 누적값 `acc`는 지금까지 처리한 문자열 길이의 합(누적 합)
  (acc, element, i) => {
    console.log("acc:", acc, "element:", element, "index:", i);
    // 현재 요소 값 `element`의 길이를 누적값에 더해 반환
    return acc + element.length;
  },
  0 // 누적값의 초기값은 0
);
console.log(totalLength);
// 출력:
// acc: 0 element: Alice index: 0
// acc: 5 element: Bob index: 1
// acc: 8 element: Charlie index: 2
// 15

reduce 메서드의 두 번째 인자를 생략하면 배열의 첫 번째 요소 값이 누적값의 초기값으로 사용되고, 해당 첫 번째 요소는 반복을 건너뜁니다. (두 번째 인자로 undefined를 지정한 경우 생략하지 않은 것으로 간주)

const array = [1, 2, 3, 4, 5];

/** 숫자로 구성된 배열 요소 값의 합을 구함 */
const sum = array.reduce((acc, element, i) => {
  console.log("acc:", acc, "element:", element, "index:", i);
  return acc + element;
});
console.log(sum);
// 출력:
// acc: 1 element: 2 index: 1
// acc: 3 element: 3 index: 2
// acc: 6 element: 4 index: 3
// acc: 10 element: 5 index: 4
// 15

원래 배열이 빈 배열일 경우 콜백 함수는 호출되지 않으며 reduce 메서드는 두 번째 인자를 그대로 반환합니다.

이때 두 번째 인자가 생략되었다면 런타임 오류가 발생합니다.

원래 배열이 한 요소만 가지고 있고 reduce 메서드의 두 번째 인자가 생략된 경우 콜백 함수는 호출되지 않으며, 그 한 요소의 값이 reduce 메서드의 반환값이 됩니다.

배열의 reduceRight 메서드는 reduce 메서드와 유사하지만 반복 순서가 반대(끝에서 시작하여 처음으로 진행)라는 점이 다릅니다.

콜백 함수의 인자 순서는 reduce 메서드와 동일하며, 누적값이 첫 번째 인자가 됩니다.

const operands = [2, 3, 3];

// 2의 (3의 3제곱) 제곱을 계산
const result = operands.reduceRight(
  (acc, element, i) => {
    console.log("acc:", acc, "element:", element, "index:", i);
    return element ** acc;
  },
  1
);
console.log(result);
// 출력:
// acc: 1 element: 3 index: 2
// acc: 3 element: 3 index: 1
// acc: 27 element: 2 index: 0
// 134217728

reduceRight 메서드의 두 번째 인자를 생략하면 배열의 마지막 요소 값이 누적값의 초기값으로 사용되고, 해당 마지막 요소는 반복을 건너뜁니다.

reduce 메서드, reduceRight 메서드의 특징

배열의 reduce 메서드와 reduceRight 메서드는 다수의 요소를 하나의 값으로 합치는 작업을 수행합니다.

함수형 프로그래밍에서는 이를 폴드(fold)라고 부릅니다.

콜백 함수가 첫 번째 인자 a와 두 번째 인자 b를 받아 a⋅b를 반환한다면(는 임의의 이항 연산), 배열 요소 값을 a0, ..., an-1로, 누적값의 초기값(각 메서드에 전달되는 두 번째 인자)을 e로 표기하면,

  • reduce 메서드의 반환값은
    (⋯((e⋅a0)⋅a1)⋯)⋅an-1
  • reduceRight 메서드의 반환값은
    (⋯((e⋅an-1)⋅an-2)⋯)⋅a0
    이 됩니다.

reduceRight 메서드에 전달되는 콜백 함수의 첫 번째 인자와 두 번째 인자를 반대로 하면 위 식은 좌우 반전된 형태가 되며, 배열 요소 값이 왼쪽에서 오른쪽 순서로 나열되고 이항 연산에 의해 오른쪽에서 왼쪽으로 접혀 들어가는 것을 알 수 있습니다.

배열 요소 값이 속한 집합 S에 이항 연산 ⋅: S × S → S와 항등원 e가 모노이드를 이루는 경우, reduce 메서드와(콜백 함수의 첫 번째 인자와 두 번째 인자를 뒤집은)reduceRight` 메서드의 반환값은 동일해집니다.

예를 들어 문자열의 집합에서 문자열 결합 (a, b) => a + b와 빈 문자열 ""은 모노이드를 이룹니다.

const array = ["a", "b", "c"];

const leftReduced = array.reduce((a, b) => a + b, "");
console.log(leftReduced); // 출력: abc

const rightReduced = array.reduceRight((b, a) => a + b, "");
console.log(rightReduced); // 출력: abc

각 반복에서 콜백 함수가 반환한 값은 다음 반복에서 동일한 콜백 함수에 첫 번째 인자로 전달됩니다.

또한 reduce 메서드나 reduceRight 메서드에 전달되는 두 번째 인자도 첫 번째 반복에서 콜백 함수에 첫 번째 인자로 전달되는 값이 됩니다.

따라서 reduce 메서드나 reduceRight 메서드의 두 번째 인자를 지정할 때 일반적으로 아래 3곳이 같은 종류의 값이 되거나 같은 종류의 값을 예상해야 합니다.

  1. 콜백 함수가 반환하는 값(그 반복까지의 누적값)
  2. reduce 메서드나 reduceRight 메서드에 전달되는 두 번째 인자(누적값의 초기값)
  3. 콜백 함수가 받는 첫 번째 인자(이전 반복까지의 누적값)

이 세 곳에 "배열 요소 값"이나 "콜백 함수가 받는 두 번째 인자"가 포함되지 않는다는 점은 주의하세요.

즉, 배열 요소 값과는 다른 종류의 값을 누적값으로 설정할 수 있습니다.

예를 들어 첫 번째 코드 예제 "배열의 각 요소 값인 문자열 길이의 합을 구함"에서는 배열 요소 값이 문자열이었지만, 위 3곳의 누적값은 문자열 길이의 합을 나타내는 숫자였습니다.

누적값 설계에 자유도가 있기 때문에 reduce 메서드나 reduceRight 메서드는 다양한 작업을 수행할 수 있는 만능 도구가 됩니다.

지금까지 소개한 대부분의 반복을 위한 메서드는 reduce 메서드나 reduceRight 메서드를 사용해도 거의 동일한 처리를 할 수 있습니다. (하지만 전용 메서드를 사용하는 것이 효율성, 가독성, 코드량 측면에서 더 뛰어난 경우가 많습니다.)

각종 메서드를 reduce 메서드로 구현하기

reduce 메서드나 reduceRight 메서드의 두 번째 인자를 생략하는 경우, 첫 번째 반복에서 첫 번째 인자로 배열의 첫 번째 요소 값이 전달되므로 배열 요소 값이 모두 같은 종류라면 일반적으로 누적값도 배열 요소 값과 같은 종류로 설정됩니다.

콜백 함수 내에서 반복 전체를 중단하고 바로 반복 이후로 이동하는 수단이 없다는 점은 forEach 메서드와 유사합니다.

현재 반복을 중단하고 다음 반복으로 넘어가려면(for 문에서의 continue 문에 해당) return 문을 사용합니다.

첫 번째 인자로 전달된 값을 그대로 반환함으로써 현재 반복이 건너뛰어졌음을 나타낼 수 있습니다.

반복 대상 인덱스

reduce 메서드나 reduceRight 메서드의 두 번째 인자를 생략하고 호출하면 배열의 길이가 0이거나 배열의 인덱스가 모두 존재하지 않을 경우 런타임 오류(TypeError)가 발생합니다.

콜백 함수의 네 번째 인자는 메서드 체인에서 이들 메서드를 호출할 때 유용할 수 있습니다(forEach 등의 콜백 함수의 세 번째 인자와 유사).

reduce 메서드, reduceRight 메서드의 대체 구현

array가 배열일 때,

let result = array.reduce(
  (acc, element) => {
    const value = <식>;
    return value;
  },
  <초기값>
);

<식>이 배열의 값을 변경하지 않는다면

let result = <초기값>;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  let acc = result;
  let element = array[i];
  const value = <식>;
  result = value;
}

와 거의 같은 의미가 됩니다(ilength<식> 안에서 참조되지 않는 변수명으로 해야 함).

또한, reduce 메서드의 두 번째 인자를 생략한 경우

let result = array.reduce(
  (acc, element) => {
    const value = <식>;
    return value;
  }
);

<식>이 배열의 값을 변경하지 않는다면

let initialized = false;
let result;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  if (!initialized) {
    result = array[i];
    initialized = true;
    continue;
  }
  let acc = result;
  let element = array[i];
  const value = <식>;
  result = value;
}
if (!initialized) throw new TypeError();

와 거의 같은 의미가 됩니다(ilength<식> 안에서 참조되지 않는 변수명으로 하고, initialized는 이 스코프에 정의되지 않은 변수명으로 해야 함).

array가 배열일 때,

let result = array.reduceRight(
  (acc, element) => {
    const value = <식>;
    return value;
  },
  <초기값>
);

<식>이 배열의 값을 변경하지 않는다면

let result = <초기값>;
for (let i = +array.length - 1; i >= 0; i--) {
  if (!(i in array)) continue;
  let acc = result;
  let element = array[i];
  const value = <식>;
  result = value;
}

와 거의 같은 의미가 됩니다(i<식> 안에서 참조되지 않는 변수명으로 해야 함).

또한, reduceRight 메서드의 두 번째 인자를 생략한 경우

let result = array.reduceRight(
  (acc, element) => {
    const value = <식>;
    return value;
  }
);

<식>이 배열의 값을 변경하지 않는다면

let initialized = false;
let result;
for (let i = +array.length - 1; i >= 0; i--) {
  if (!(i in array)) continue;
  if (!initialized) {
    result = array[i];
    initialized = true;
    continue;
  }
  let acc = result;
  let element = array[i];
  const value = <식>;
  result = value;
}
if (!initialized) throw new TypeError();

와 거의 같은 의미가 됩니다(i<식> 안에서 참조되지 않는 변수명으로 하고, initialized는 이 스코프에 정의되지 않은 변수명으로 해야 함).


includes 메소드

배열의 includes 메소드는 지정된 값을 배열의 각 요소와 비교하여, 동일한 요소가 있으면 true를 반환합니다.

어떤 요소와도 값이 같지 않으면 false를 반환합니다.

이 메소드에는 콜백 함수를 전달할 수 없습니다. 대신, 첫 번째 인수로 비교할 값을 지정합니다.

const array = ["a", "b", "c", "d", "e"];

console.log(array.includes("c")); // true
console.log(array.includes("f")); // false

두 번째 인수로는 비교를 시작할 인덱스를 지정할 수 있습니다. 기본값은 0이며, 모든 요소가 비교됩니다.

이 인수에 음수를 지정하면 배열의 끝에서부터 셉니다.

const array = ["a", "b", "c", "d", "e"];

console.log(array.includes("c", 2)); // true (인덱스 2의 요소가 "c"이므로 true)
console.log(array.includes("c", 3)); // false (인덱스 3 이후에 "c"는 없으므로 false)

console.log(array.includes("c", -3)); // true (끝에서 세어 3번째 = 인덱스 2부터 비교 시작)
console.log(array.includes("c", -2)); // false (끝에서 세어 2번째 = 인덱스 3부터 비교 시작)

includes 메소드의 특징

배열의 includes 메소드는 콜백 함수를 받지 않기 때문에 반복 처리를 직접 지정할 수 없습니다.

대신, 자주 사용되는 유형의 반복 처리에 특화된 메소드로 이해할 수 있습니다.

includes 메소드는 지정된 값이 배열에 존재하는지 여부를 확인하는 데 사용됩니다.

빈 배열에 대해 includes 메소드를 호출하면 항상 false를 반환합니다.

배열의 includes 메소드는 동일한지 여부를 판단할 때 SameValueZero 추상 연산을 사용합니다.

const sameValueZero = (a, b) => a === b || (a !== a && b !== b);

SameValueZero 추상 연산

비교할 인덱스의 범위는 배열의 includes 메소드가 배열 비슷한 객체에도 호출될 수 있습니다.

includes 메소드는 콜백 함수를 받지 않기 때문에, (처리에 걸리는 시간 등을 제외하고) 반복이 수행되었는지 또는 반복 순서를 코드에서 관찰할 수 없는 경우가 있습니다.

처리 시스템은 그러한 경우에 기본 반복 대신 고속 알고리즘을 사용하는 등의 최적화를 수행할 수 있습니다.

includes 메소드의 재작성

여기서 === 연산자를 사용하는 부분은 NaN에 대한 비교에서 동작이 일치하지 않습니다.

엄밀한 재작성에는 앞서 언급한 sameValueZero 함수를 사용해야 합니다.

배열이 array일 때,

const search = <value>;
let result = array.includes(search);

이는

const search = <value>;
let result = array.findIndex((element) => element === search) !== -1;

또는

const search = <value>;
let result = false;
for (let i = 0, length = +array.length; i < length; i++) {
  if (array[i] === search) {
    result = true;
    break;
  }
}

또는

const search = <value>;
let result = false;
for (const element of array) {
  if (element === search) {
    result = true;
    break;
  }
}

와 같은 의미입니다.

배열이 밀집 배열일 때,

const search = <value>;
const result = array.includes(search);

const search = <value>;
const result = array.some((element) => element === search);

와 같은 의미입니다.

배열이 밀집 배열일 때,

const result = array.includes(<value>, <startIndex>);

에서 <value>NaN이 아닌 경우

const result = array.indexOf(<value>, <startIndex>) !== -1;

와 같은 의미입니다. (<startIndex>는 생략 가능합니다)

indexOf 메소드, lastIndexOf 메소드

배열의 indexOf 메소드는 지정된 값을 배열의 각 요소와 순서대로 비교하여, 동일한 요소가 있으면 그 인덱스를 반환합니다. 어떤 요소와도 값이 같지 않으면 -1을 반환합니다.

이 메소드에는 콜백 함수를 전달할 수 없습니다. 대신, 첫 번째 인수로 비교할 값을 지정합니다.

const array = ["a", "b", "c", "b", "a"];

console.log(array.indexOf("b")); // 1
console.log(array.indexOf("d")); // -1

동일한 요소가 여러 개 있을 경우, 가장 앞에 있는 요소의 인덱스가 반환됩니다.

두 번째 인수로는 비교를 시작할 인덱스를 지정할 수 있습니다. 기본값은 0이며, 모든 요소가 비교됩니다.

이 인수에 음수를 지정하면 배열의 끝에서부터 셉니다. (includes 메소드와 동일)

배열의 lastIndexOf 메소드는 indexOf 메소드와 유사하지만, 배열의 끝에서부터 역순으로 비교한다는 점이 다릅니다.

따라서, 동일한 요소가 여러 개 있을 경우, 가장 뒤에 있는 요소의 인덱스가 반환됩니다.

const array = ["a", "b", "c", "b", "a"];

console.log(array.lastIndexOf("b")); // 3
console.log(array.lastIndexOf("d")); // -1

두 번째 인수로는 비교를 시작할 인덱스를 지정할 수 있습니다.

기본값은 배열의 길이 - 1이며, 모든 요소가 비교됩니다.

이 인덱스부터 시작하여 앞쪽으로 비교가 이루어집니다. 이 인수에 음수를 지정하면 배열의 끝에서부터 셉니다.

const array = ["a", "b", "c", "b", "a"];

console.log(array.lastIndexOf("b", 1)); // 1 (인덱스 1의 요소가 "b")
console.log(array.lastIndexOf("b", 0)); // -1 (인덱스 0까지 "b"는 없음)

console.log(array.lastIndexOf("b", -4)); // 1 (끝에서 세어 4번째 = 인덱스 1부터 시작)
console.log(array.lastIndexOf("b", -5)); // -1 (끝에서 세어 5번째 = 인덱스 0부터 시작)

indexOf 메소드, lastIndexOf 메소드의 특징

배열의 indexOf 메소드와 lastIndexOf 메소드는 콜백 함수를 받지 않기 때문에 반복 처리를 직접 지정할 수 없습니다.

대신, 자주 사용되는 유형의 반복 처리에 특화된 메소드로 이해할 수 있습니다.

indexOf 메소드와 lastIndexOf 메소드는 지정된 값이 배열에 존재하는지 여부 및 어느 위치에 존재하는지를 확인하는 데 사용됩니다.

빈 배열에 대해 indexOf 메소드나 lastIndexOf 메소드를 호출하면 항상 -1을 반환합니다.

배열의 indexOf 메소드와 lastIndexOf 메소드는 동일한지 여부를 판단할 때 === 연산자와 동일한 IsStrictlyEqual 추상 연산을 사용합니다.

IsStrictlyEqual 추상 연산

비교할 인덱스의 범위는 배열의 indexOf 메소드와 lastIndexOf 메소드가 배열 비슷한 객체에도 호출될 수 있습니다.

indexOf 메소드나 lastIndexOf 메소드는 콜백 함수를 받지 않기 때문에, (처리에 걸리는 시간 등을 제외하고) 반복이 수행되었는지 또는 반복 순서를 코드에서 관찰할 수 없는 경우가 있습니다.

처리 시스템은 그러한 경우에 기본 반복 대신 고속 알고리즘을 사용하는 등의 최적화를 수행할 수 있습니다.

indexOf 메소드, lastIndexOf 메소드의 재작성

배열이 array일 때,

const search = <value>;
let result = array.indexOf(search);

const search = <value>;
let result = -1;
for (let i = 0, length = +array.length; i < length; i++) {
  if (!(i in array)) continue;
  if (array[i] === search) {
    result = i;
    break;
  }
}

와 같은 의미이며,

const search = <value>;
let result = array.lastIndexOf(search);

const search = <value>;
let result = -1;
for (let i = +array.length - 1; i >= 0; i--) {
  if (!(i in array)) continue;
  if (array[i] === search) {
    result = i;
    break;
  }
}

와 같은 의미입니다.

배열이 밀집 배열일 때,

const search = <value>;
let result = array.indexOf(search);

const search = <value>;
let result = array.findIndex((element) => element === search);

또는

const search = <value>;
let result = false;
for (const element of array) {
  if (element === search) {
    result = true;
    break;
  }
}

와 같은 의미이며,

const search = <value>;
let result = array.lastIndexOf(search);

const search = <value>;
let result = array.findLastIndex((element) => element === search);

와 같은 의미입니다.

존재 여부 판단의 재작성

배열이 밀집 배열일 때,

const search = <value>;
let result = array.includes(search);

const search = <value>;
let result = array.some((element) => element === search);

와 같은 의미입니다.

entries 메소드, keys 메소드, values 메소드, Symbol.iterator 메소드

배열의 이 메소드들은 단독으로 반복을 수행하지 않지만, 반복 처리에서 중요한 역할을 하기 때문에 소개합니다.

이 메소드들은 공통적으로 인수를 받지 않으며, 반환 값으로 배열 이터레이터 객체를 반환합니다.

배열 이터레이터는 이터레이터의 한 종류로, 이를 이용해 배열의 요소 값을 순차적으로 얻을 수 있습니다.

이터레이터에서 값을 순차적으로 꺼내기 위해서는 이터레이터의 next 메소드를 호출합니다.

next 메소드는 값을 생성할 때 { value: <value>, done: false } 형태의 객체를 반환하며, 더 이상 생성할 값이 없을 때는 { value: <returnValue>, done: true } 형태의 객체를 반환합니다. <returnValue>는 반복의 대상이 아니며, 배열 이터레이터의 경우 undefined가 됩니다.

이터레이터는 그 자체로 반복 가능한 객체이기 때문에, 이터레이터 it이 값을 유한 개 생성하고 종료하는 경우, 표현식 [...]으로 생성된 값을 하나의 배열로 모을 수 있습니다.

배열의 entries 메소드는 배열의 각 요소에 대해 인덱스와 값의 쌍(두 개의 요소를 가진 배열)을 순차적으로 생성하는 이터레이터를 반환합니다.

const array = ["a", "b", "c"];

const it = array.entries();
console.log([...it]); // 출력: [ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ]

배열의 keys 메소드는 배열의 각 요소의 인덱스를 순차적으로 생성하는 이터레이터를 반환합니다.

const array = ["a", "b", "c"];

const it = array.keys();
console.log([...it]); // 출력: [ 0, 1, 2 ]

배열의 values 메소드는 배열의 각 요소의 값을 순차적으로 생성하는 이터레이터를 반환합니다.

const array = ["a", "b", "c"];

const it = array.values();
console.log([...it]); // 출력: [ 'a', 'b', 'c' ]

배열의 Symbol.iterator 메소드는 values 메소드와 동일합니다.

const array = ["a", "b", "c"];

const it = array[Symbol.iterator]();
console.log([...it]); // 출력: [ 'a', 'b', 'c' ]

console.log(array.values === array[Symbol.iterator]); // 출력: true

entries 메소드, keys 메소드, values 메소드, Symbol.iterator 메소드의 특징

배열의 entries 메소드는 요소의 인덱스와 값을 모두 사용하는 반복 처리에 유용합니다.

keys 메소드는 인덱스만 사용하는 반복 처리에 유용합니다.

특히, for-of 문에서 배열을 반복 대상으로 하면 배열의 요소 값만 추출됩니다.

entries 메소드를 사용하면 인덱스와 값을 모두 추출하는 반복 처리를 할 수 있습니다.

또는 keys 메소드를 사용하면 인덱스만 추출하는 반복 처리를 할 수 있습니다.

const array = ["a", "b", "c"];

for (const [index, value] of array.entries()) {
  console.log(index, value);
}
// 출력:
// 0 a
// 1 b
// 2 c

for (const index of array.keys()) {
  console.log(index);
}
// 출력:
// 0
// 1
// 2

배열 이외의 객체에도 동일한 메소드가 존재할 수 있습니다.

예를 들어, MapSet에도 entries 메소드, keys 메소드, values 메소드가 있습니다.

해당 객체에 포함된 요소 값을 하나씩 추출하기 위한 통일된 방법으로 이 메소드들이 제공됩니다.

const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (const [key, value] of map.entries()) {
  console.log(key, value);
}
// 출력:
// a 1
// b 2
// c 3

const set = new Set(["a", "b", "c"]);
for (const [key, value] of set.entries()) {
  console.log(key, value);
}
// 출력:
// a a
// b b
// c c

values 메소드는 이러한 통일된 방법의 일부로 배열에도 편의상 제공된 것입니다.

values 메소드를 호출하는 객체가 배열임을 알고 있는 경우 굳이 사용할 필요는 없습니다.

한편 배열의 Symbol.iterator 메소드는 배열이 반복 가능한 객체임을 위해 필요한 메소드입니다.

for-of 문이나 스프레드 구문 등에서 배열을 반복 처리할 때는 암묵적으로 이 메소드가 호출됩니다.

for-of 문에서 배열의 요소 값이 순차적으로 추출되는 이유는 배열의 Symbol.iterator 메소드가 배열의 요소 값을 순차적으로 생성하는 이터레이터를 반환하기 때문입니다.

배열 이터레이터는 next 메소드가 호출될 때마다 배열의 길이를 확인하여 종료를 판단합니다.

따라서, 반복 중에 배열의 길이가 증가하거나 감소하더라도 배열의 끝까지 올바르게 값을 생성합니다.

배열의 프로퍼티가 존재하지 않는 (빈) 인덱스도 반복 대상이 됩니다.

이 경우 값은 undefined가 됩니다.

const array = ["a", , "c"];

const it = array.entries();
console.log([...it]); // 출력: [ [ 0, 'a' ], [ 1, undefined ], [ 2, 'c' ] ]

배열의 entries 메소드, keys 메소드, values 메소드, Symbol.iterator 메소드는 배열 비슷한 객체에도 호출될 수 있습니다.

const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

const it = Array.prototype.entries.call(arrayLike);
console.log([...it]); // 출력: [ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ]

이상으로 배열의 includes, indexOf, lastIndexOf 메소드와 반복 관련 메소드들에 대해 알아보았습니다.

이 메소드들은 자주 사용되는 메소드들이며, 배열의 요소를 효율적으로 찾거나 반복하는 데 매우 유용합니다.

따라서, 이러한 메소드들의 동작 원리와 특징을 잘 이해하고 사용하는 것이 중요합니다.


Array.from 메서드

Array 생성자의 from 메서드는 반복 가능한 객체나 배열과 유사한 객체를 새로운 배열로 변환하여 반환합니다.

이 메서드는 배열 메서드가 아니라는 점을 주의하세요. Array.from(o) 형태로 호출됩니다.

이 메서드는 하나의 필수 인자와 두 개의 선택적 인자를 받습니다.

필수 인자인 첫 번째 인자에는 변환 대상인 반복 가능한 객체나 배열과 유사한 객체를 지정합니다.

// `Set`은 반복 가능한 객체입니다.
const iterable = new Set(["a", "b", "c"]);
console.log(iterable); // 출력: Set(3) { 'a', 'b', 'c' }

const array = Array.from(iterable);
console.log(array); // 출력: [ 'a', 'b', 'c' ]

// 배열과 유사한 객체
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

const array2 = Array.from(arrayLike);
console.log(array2); // 출력: [ 'a', 'b', 'c' ]

from 메서드의 두 번째 인자에는 각 요소에 대해 순차적으로 호출되는 콜백 함수를 지정할 수 있습니다.

이 콜백 함수는 배열의 map 메서드에 전달하는 함수와 유사하며, 반환값은 새 배열에 추가될 요소의 값이 됩니다.

콜백 함수에는 두 개의 인자가 전달됩니다.

첫 번째 인자는 변환 대상 요소이며, 두 번째 인자는 그 요소의 인덱스입니다.

const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };

const array = Array.from(arrayLike, (element, index) => {
  console.log(element, index);
  return element.toUpperCase(); // 새로운 배열의 요소 값에는 대문자로 변환된 문자열을 사용
});
console.log(array);
// 출력:
// a 0
// b 1
// c 2
// [ 'A', 'B', 'C' ]

반환값은 빽빽한 배열이 됩니다.

그 길이는 from 메서드의 첫 번째 인자가 반복 가능한 객체인 경우, 해당 객체에서 추출된 요소 수와 동일하며, 반복 가능하지 않은 배열과 유사한 객체인 경우 그 길이와 같습니다.

Array.from 메서드의 특징

Array.from 메서드는 반복 가능한 객체나 배열과 유사한 객체를 배열로 변환하기 위한 메서드입니다.

반복 가능한 객체는 Symbol.iterator 메서드를 가진 객체입니다.

그대로도 for-of 문을 통해 반복 처리가 가능하지만 배열에 존재하는 메서드를 사용해 처리하거나 인덱스로 요소에 접근하고자 할 때 Array.from 메서드가 유용합니다.

배열과 유사한 객체도 마찬가지로 배열에 존재하는 메서드를 사용해 처리하기 위해 Array.from 메서드를 사용할 수 있습니다.

Array.from에 배열과 유사한 객체를 전달할 경우, 그 객체에 프로퍼티가 존재하지 않는 (비어 있는) 인덱스도 반복 대상이 됩니다.

const arrayLike = { 0: "a", 2: "c", length: 3 };

const array = Array.from(arrayLike);
console.log(array); // 출력: [ 'a', undefined, 'c' ]

const array2 = Array.from(arrayLike, (element, index) => {
  console.log(element, index);
  return element?.toUpperCase();
});
console.log(array2);
// 출력:
// a 0
// undefined 1
// c 2
// [ 'A', undefined, 'C' ]

이 특징을 이용해 특정 길이의 빽빽한 배열을 Array.from 메서드로 생성할 수 있습니다.

const length = 5;

const array = Array.from({ length }, (_, index) => index);
console.log(array); // 출력: [ 0, 1, 2, 3, 4 ]

// `new Array(length)`는 요소의 프로퍼티가 존재하지 않는 드문 배열이 생성되므로 `map` 메서드를 사용해도 요소가 생성되지 않습니다.
const array2 = new Array(length).map((_, index) => index);
console.log(array2); // 출력: [ <5 empty items> ]

2차원 배열을 생성할 때도 유용합니다.

const rows = 3;
const columns = 4;

// 2차원 배열을 생성합니다.
const array = Array.from({ length: rows }, () => {
  return new Array(columns).fill(0);
});
console.log(array);
// 출력: [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ] ]

배열도 반복 가능한 객체이므로 Array.from 메서드의 첫 번째 인자로 전달할 수 있습니다.

이 경우 반환값은 새로운 배열이 됩니다.

const array = ["a", "b", "c"];

const newArray = Array.from(array);
console.log(newArray); // 출력: [ 'a', 'b', 'c' ]
console.log(array === newArray); // 출력: false

// 드문 배열은 빽빽한 배열로 변환됩니다.
const sparseArray = ["a", , "c"];

const newArray2 = Array.from(sparseArray, (element, index) => {
  return element?.toUpperCase();
});
console.log(newArray2); // 출력: [ 'A', undefined, 'C' ]

Array.from 메서드에 전달하는 콜백 함수를 비동기 함수로 만들면 from 메서드의 반환값은 Promise로 구성된 배열이 됩니다.

이는 배열의 map 메서드와 동일합니다.

Array.from 메서드의 첫 번째 인자로 반복 가능하지 않은 배열과 유사한 객체를 전달할 경우, Array.from 메서드가 반복하는 범위와 반환값이 되는 새 배열의 길이는 메서드가 호출된 시점에 배열과 유사한 객체의 길이를 기준으로 결정됩니다.

따라서 콜백 함수에서 배열과 유사한 객체의 길이를 늘린 경우 원래 길이를 초과하는 인덱스의 요소는 반복되지 않으며 새 배열에도 포함되지 않습니다.

Array.from 메서드의 세 번째 인자로 콜백 함수 내에서 this로 사용될 값을 지정할 수 있습니다. (배열의 forEach 메서드의 두 번째 인자와 동일)

Array.from 메서드는 Array 이외의 생성자에 대해서도 호출할 수 있습니다.

이 경우 반환값은 해당 생성자의 인스턴스가 됩니다.

const iterable = new Set(["a", "b", "c"]);

// 배열과 유사한 객체를 생성합니다.
const array = Array.from.call(Object, iterable);
console.log(array); // 출력: { '0': 'a', '1': 'b', '2': 'c', length: 3 }

class ArraySubclass extends Array {}
const arraySubclass = ArraySubclass.from(iterable);
console.log(arraySubclass); // 출력: ArraySubclass(3) [ 'a', 'b', 'c' ]

Array.from 메서드의 대체 구현

iterable이 반복 가능한 객체일 때,

const array = Array.from(iterable);

는 다음과 거의 동일합니다.

const array = [...iterable];

또는

const [...array] = iterable;

또는

const array = [];
for (const element of iterable) {
  array.push(element);
}

iterable이 반복 가능한 객체일 때,

const array = Array.from(iterable, (element) => {
  const result = <식>;
  return result;
});

는 다음과 거의 동일합니다.

const array = [];
for (const element of iterable) {
  const result = <식>;
  array.push(result);
}

arrayLike가 배열과 유사한 객체일 때,

const array = Array.from(arrayLike);

는 다음과 거의 동일합니다.

const array = [];
for (let i = 0, length = +arrayLike.length; i < length; i++) {
  array.push(arrayLike[i]);
}

또는

const array = Array.prototype.slice.call(arrayLike, 0).fill(undefined);

또는

const array = [...Array.prototype.values.call(arrayLike)];