자바스크립트/모던 자바스크립트

모던 자바스크립트, 편의 유틸 배열 메서드

Jake Seo 2023. 3. 13. 00:43

새로운 배열 함수 추가

  • ES2015+ 이후에 배열에서 새로운 메서드가 많이 추가되었다.

Array.of()

  • 이산 인수로 전달한 값을 배열로 만들어 반환해준다.
const arr = Array.of("a", "b", "c");
console.log(arr); // ['a', 'b', 'c']

기능은 알겠는데, 언제 유용할까?
사실 배열 리터럴([]) 이 더 간략하다.

서브 클래스로의 활용

class MyArray extends Array {
  print() {
    console.log(`[${this.join(", ")}]`);
  }
  sum() {
    return this.reduce((ac, cur) => ac + cur, 0);
  }
}

const myArray = MyArray.of(1, 2, 3);
myArray.print(); // [1, 2, 3]
console.log(myArray.sum()); // 6
  • MyArrayArray 를 상속하면, 따로 생성자를 만들 필요가 사라진다.
    • of() 메서드도 같이 상속 받기 때문에 Array.of() 는 서브클래스가 사용할 용도로 좋다.

Array.from()

  • 인자를 기반으로 배열을 생성한다.
    • 타입에 따라 알맞게 배열을 생성해준다.
Array.from(arrayLike);
Array.from(arrayLike, mapFn, thisArg);

문자열로 생성하기

const str = "123";
const arr = Array.from(str);
console.log(arr); // (3) ['1', '2', '3']
  • 이모지와 같이 4바이트로 이루어진 UTF-16 문자가 와도 적절하게 쪼개준다.

split 이 따로 필요 없다.

'배열과 비슷한 객체'로 생성하기

const arr = Array.from({ length: 2, 0: "one", 1: "two" });
console.log(arr); // (2) ['one', 'two']
  • '배열과 비슷한 객체'란 객체인데 프로퍼티 이름이 숫자이며, 프로퍼티가 몇개 들어있는지에 대한 정보를 length 로 제공하는 객체이다.
    • 사실 자바스크립트의 기본 배열은 전부 '배열과 비슷한 객체'이다.
    • 자바스크립트에서 ArrayBuffer 가 진짜 다른 언어에서 지원하는 배열과 같은 것이다.

생성하고 값 매핑하기

const str = "123";
const arr = Array.from(str, Number);
console.log(arr); // (3) [1, 2, 3]
  • 모든 원소에 Number(element) 를 한 것과 동일한 결과다.
  • 두번째 인자는 mapFn 으로 매핑 함수를 넣어줄 수 있다.
    • mapFn(value, index) 의 형태로 실행되는 것을 명심해야 한다.
      • Number() 는 2번째 인자가 씹혀서 자동으로 첫번째 인자인 value 만 적용된다.
      • parseInt() 의 경우엔 2번째 인자가 진법을 나타내기 때문에 n번째 인덱스에 n진법이 적용되어 버려 의도치 않은 결과를 낼 수 있어 주의해야 한다.
const str = "123";
const arr = Array.from(str, parseInt);
console.log(arr); // (3) [1, NaN, NaN]
  • 의도치 않게 원하지 않는 결과를 얻을 수 있다.
  • 1 에선 radix0 이 들어가 자동으로 10진법이 적용됐다.
  • 2 에선 1 이 들어가 1 진법이 적용됐는데 1 진법은 지원하지 않는다. -> NaN
  • 3 에선 2 가 들어가 2 진법이 적용됐는데 2 진법에서 3 이란 숫자는 사용할 수 없다.
const str = "123";
const mapToInt = (n) => parseInt(n, 10);
const arr = Array.from(str, mapToInt);
console.log(arr); // (3) [1, 2, 3]
  • 기수(radix) 를 고정한 새로운 매핑 함수를 정의하여 간단히 해결할 수 있다.
  • 이 매핑 함수는 첫번째 인자만 사용하므로 인덱스는 무시된다.

범위 배열 만들기

  • 아래의 예제는 arrayLike 오브젝트를 제공하고, mapFn 을 제공하여 범위 배열을 만든 예이다.
  • arrayLike 배열에 length 가 있어서 arrayLike 객체의 길이는 100 으로 인식되고, 내부의 원소는 undefined 로 취급되고 있다.

arrayLike 배열로 구성

const arr100 = Array.from({ length: 100 }, (_, index) => index);
console.log(arr100); // (100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

Array 객체로 구성

const arr100 = Array.from(Array(100), (_, index) => index);
console.log(arr100); // (100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
  • Array.from() 에서 인자로 넣는 mapFn 함수는 일반적인 map() 함수를 쓰는 것과 큰 차이가 있다.
    • 배열 내부의 값이 초기화되지 않은 상태여도 매핑함수를 수행한다는 것이다.
const arr100 = Array(100).map((_, i) => i);
console.log(arr100); // (100) [empty × 100]

const arr100_2 = Array(100)
  .fill()
  .map((_, i) => i);
console.log(arr100_2); // (100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
  • 단순히 Array(100).map() 으로 매핑 함수를 적용하면 결과에 원하지 않는 값이 등장한다.
  • arr100_2 의 예제처럼 .fill() 메서드를 중간에 끼워넣어야 map() 메서드의 적용 범위 안에 들어간다.
  • 이러한 점 때문에 Array.from() 이 범위 배열 생성 시에 좀 더 안정적으로 매핑함수를 적용할 수 있다.

유틸 함수 구성

const rangeArray = (start, end, step = 1) =>
  Array.from(
    { length: Math.floor(Math.abs(end - start) / Math.abs(step)) },
    (_, i) => start + i * step
  );

Arrays.prototype.keys()

  • 배열 키를 순회하는 이터레이터 를 반환한다.
  • 배열 엔트리의 이름은 기술적으로 문자열이지만, 숫자를 반환한다.
  • 희소 배열이라도 0 <= n <= length 범위의 모든 인덱스 값이 이터레이터에 의해 반환된다.
  • 배열 인덱스가 아닌 열거 가능한 프로퍼티가 있더라도 무시된다.
const a = ["one", "two", "three"];

for (const index of a.keys()) {
  console.log(index);
}

/*
출력 결과:
0
1
2
/*

Object.keys() 와의 비교

  • Object.keys()Arrays.prototype.keys() 는 유사한 목적을 가졌지만 동작이 다르다.
  • Object.keys() 는 말 그대로 객체를 위한 메서드인데, 자바스크립트의 배열도 객체기 때문에 Object.keys() 가 동작한다.

차이점은 아래와 같다.

  • 반환 타입이 다르다.
    • Object.keys(): 배열을 반환한다.
    • Arrays.prototype.keys(): 이터레이터를 반환한다.
const a = [1, 2, 3];
console.log(Object.keys(a)); // (3) ['0', '1', '2']
console.log(a.keys()); // Array Iterator {}
  • 반환 값이 다르다.
    • Object.keys(): 문자열을 반환한다. ex) ['1', '2', '3']
    • Arrays.prototype.keys(): 숫자를 반환한다. ex) 1, 2, 3
const a = [1, 2, 3];

const objKeys = Object.keys(a);
const protoKeys = [];

for (const e of a.keys()) {
  protoKeys.push(e);
}

console.log(objKeys); // (3) ['0', '1', '2']
console.log(protoKeys); // (3) [0, 1, 2]
  • 공백인 인덱스에 대한 동작이 다르다.
    • Object.keys(): 공백인 인덱스도 순회한다.
    • Arrays.prototype.keys(): 공백인 인덱스를 무시한다.
const sparseArr = [1, , , , , , , 2, 3];
const protoKeys = [];

for (const e of sparseArr.keys()) {
  protoKeys.push(e);
}

console.log(protoKeys); // (9) [0, 1, 2, 3, 4, 5, 6, 7, 8]
console.log(Object.keys(sparseArr)); // (3) ['0', '7', '8']
  • 열거 가능한 속성에 대한 동작이 다르다.
    • Object.keys(): 열거 가능한 속성이 있다면, 결과에 포함한다.
    • Arrays.prototype.keys(): 열거 가능한 속성이 있어도 무시하고, 오직 인덱스만 순회한다.
const arr = [1, 2, 3];
arr.enumerableKey = "HELLO";

const protoKeys = [];

for (const e of arr.keys()) {
  protoKeys.push(e);
}

console.log(protoKeys); // (3) [0, 1, 2]
console.log(Object.keys(arr)); // ['0', '1', '2', 'enumerableKey']

Arrays.prototype.values()

  • 값을 순회하는 이터레이터를 반환한다.
const arr = [1, 2, 3];

for (const v of arr.values()) {
  console.log(v); // 1, 2, 3
}

Object.values() 와의 비교

  • 위에서 Arrays.prototype.keys()Object.keys() 를 비교한 것과 대체로 같은 공통점을 갖는다.
  • 비어있는 값도 길이에 맞게 잘 순회한다.
const arr = [1, , , , , 2, 3];

const prototypeValues = [];

for (const v of arr.values()) {
  prototypeValues.push(v);
}

console.log(prototypeValues); // (7) [1, undefined, undefined, undefined, undefined, 2, 3]
console.log(Object.values(arr)); // (3) [1, 2, 3]

Arrays.prototype.entries()

  • 엔트리를 순회하는 이터레이터를 반환한다.
    • 엔트리란 객체에선 [키, 값] 을 엔트리라고 한다.
    • 배열은 arrayLike 객체기 때문에 [인덱스, 값] 이 반환된다.
const arr = ["one", "two", "three"];
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}

/*
출력 결과:
0 'one'
1 'two'
2 'three'
*/

Object.entries() 와 비교

  • 위에서 비교했던 2가지 메서드와 같다.
const arr = [, undefined, , , "y"];

for (const [index, value] of arr.entries()) {
  console.log(index, value, index in arr ? "present" : "absent");
}

/*
출력 결과:
0 undefined 'absent'
1 undefined 'present'
2 undefined 'absent'
3 undefined 'absent'
4 'y' 'present'
*/
  • 위의 예에서 in 연산자는 앞쪽 피연산자가 뒤에 오는 피연산 객체의 프로퍼티로 존재하는지를 판단한다.
  • 아무것도 넣지 않으면, arrayLike 객체는 인덱스 키를 갖지 않지만, undefined 를 명시적으로 넣으면 인덱스 키를 갖기 때문에 present 가 출력된다.

Array.prototype.copyWithin()

  • 배열 하나에서 원소를 복사할 때 이용하는 메서드이다.
  • 범위를 지정해 배열의 원소를 복사할 수 있다.
    • target, start, end 를 순서대로 인자로 받는다.
  • 기존의 배열을 수정하고, 수정된 배열을 반환하는 것도 동시에 한다.
  • 단순 for 문으로 배열을 복사하는 경우 생길 수 있는 문제들을 해결할 수 있는 기능을 품고 있다.

단순 예제

const a = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"];
console.log("before: ", a); // (11) ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']
a.copyWithin(2, 8);
console.log("after: ", a); // (11) ['a', 'b', 'i', 'j', 'k', 'f', 'g', 'h', 'i', 'j', 'k']
  • target 인덱스가 2 이고 start 인덱스가 8 이었다.
  • 2 번 인덱스의 알파벳이 c 였고, 8 번 인덱스의 알파벳이 i 였다.
  • c 자리에 i, j, k 가 순서대로 복사된 것을 볼 수 있다.
  • 8 번 인덱스의 알파벳 이후로 쭉 복사된 것이다.

문제 해결 예제

const a = ["a", "b", "c", "d", "e", "f", "g"];
console.log("before: ", a); // (7) ['a', 'b', 'c', 'd', 'e', 'f', 'g']
a.copyWithin(4, 2);
console.log("after: ", a); // (7) ['a', 'b', 'c', 'd', 'c', 'd', 'e']
  • 위의 예제는 일반적인 for 문을 이용한 복사였으면 결과가 다르다.
    • 마지막에 eg 에 넣어야 하는 순간 e 에 새로 복사된 c 가 있기 때문이다.
    • 그래서 인덱스를 이용하는 for 문을 통해 복사했다면, 마지막이 c, d, c 로 끝났을 것이다.
    • copyWithin() 은 해당 엘리먼트가 복사 전의 상태인지 확인한다.
  • 어레이가 확장되지 않고 복사가 중지된 모습도 볼 수 있다.
    • 정해진 길이를 벗어나지 않는다.

이렇게 특이하게 동작하는 이유는 그래픽 작업에서 이러한 동작을 필요로 하기 때문이다.

공백 처리 예제

const arrayString = (a) => {
  return Array.from(a.keys(), (key) => {
    return key in a ? a[key] : "*공백*";
  }).join(", ");
};

const a = ["a", "b", "c", "d", , "f", "g"];
console.log(arrayString(a)); // a, b, c, d, *공백*, f, g
a.copyWithin(1, 3);
console.log(arrayString(a)); // a, d, *공백*, f, g, f, g
  • 공백마저 복사한다.

Array.prototype.find()

  • 첫번째로 일치하는 원소를 찾는다.
  • 찾지 못하면 undefined 를 반환한다.

기본 예제

const a = [1, 2, 3, 4, 5, 6];
const find2 = a.find((v) => v === 2);
const find7 = a.find((v) => v === 7);

console.log(find2); // 2
console.log(find7); // undefined

빈 배열 예제

const a = [];
const findTrue = a.find((v) => {
  console.log("hello");
  return true;
});

console.log(findTrue); // undefined
  • 빈 배열을 대상으로 했을 때는 바로 undefined 를 반환한다.

배열을 중간에 수정하는 예제

const a = ["one", "two", "three"];
const x = a.find((value, index) => {
  console.log(`Visiting index ${index}: ${value}`);

  if (index === 0) {
    a[2] = a[2].toUpperCase();
  } else if (index === 1) {
    a.push("four");
  }

  return value === "four";
});

console.log(x); // undefined
/*
출력결과:
Visiting index 0: one
Visiting index 1: two
Visiting index 2: THREE
undefined
*/
  • 순회 중간에 배열에 원소가 추가되어도 추가된 원소까지 순회하지 않는다.
  • 그러한 이유로 원소를 찾지 못해 undefined 를 반환한다.
const x = [1, 2, 3];

for (let i = 0; i < x.length; i++) {
  if (i === 1) {
    x.push(4);
  }

  console.log(x[i]);
}
/*
출력결과:
1
2
3
4
*/
  • 일반 for 문은 length 를 기준으로 하고 중간에 length 가 업데이트 되기 때문에 추가된 원소까지 순회한다.

Array.prototype.findIndex()

  • find() 와의 유일한 차이는 인덱스를 반환한다는 것 뿐이다.
const arr = [1, 2, 3, 4, 5];

const index = arr.findIndex((v) => v === 5);
console.log(index); // 4
  • 5 는 4번째 인덱스에 있다.

Array.prototype.fill()

  • 배열에 특정한 값을 채운다.
const a = Array(5).fill("hello");
console.log(a); // (5) ['hello', 'hello', 'hello', 'hello', 'hello']

객체를 사용할 때 빠질 수 있는 함정

const a = Array(2).fill({});
a[0].name = "jake";
a[1].name = "jack";

console.log(a);

/*
출력 결과:
0: {name: 'jack'}
1: {name: 'jack'}
*/
  • 분명 의도는 0 번 인덱스에는 name 프로퍼티에 jake 값을 넣고 1 번엔 jack 을 넣는 것이었다.
  • 이렇게 결과가 나온 이유는 객체가 참조하는 주소가 둘 다 똑같아서 그렇다.
  • Array.prototype.fill() 은 순회하지 않고 한번에 값을 넣는다는 것을 알아야 한다.
const a = Array.from({ length: 2 }, () => ({}));
a[0].name = "jake";
a[1].name = "jack";

console.log(a);

/*
출력 결과:
0: {name: 'jake'}
1: {name: 'jack'}
*/
  • Array.from() 을 이용해 해결하는 방법이다.
const a = Array(2)
  .fill()
  .map(() => ({}));

a[0].name = "jake";
a[1].name = "jack";

console.log(a);

/*
출력 결과:
0: {name: 'jake'}
1: {name: 'jack'}
*/
  • Array.prototype.fill()Array.prototype.map() 을 연계해 해결하는 방법이다.

Array.prototype.includes()

  • 주어진 값이 배열에 있는지 확인한다.
const a = ["hello", "world"];
console.log(a.includes("world")); // true
console.log(a.includes("korea")); // false

SameValueZero 알고리즘

  • Array.prototype.includes()SameValueZero 알고리즘에 의해 주어진 값이 배열에 있는지 판단한다.
    • 이는 indexOf() 와 같은 메서드에서 사용하는 엄격한 동등성 비교 알고리즘 과 다르다.
    • NaN 의 처리에서 둘의 결과가 다르다.
    • 엄격한 동등성 비교 알고리즘NaNNaN 을 정확히 비교하지 못한다.
const a = [NaN];
console.log(a.includes(NaN)); // true
console.log(a.indexOf(NaN)); // -1

이 외에 [-0].includes(+0) 은 엄격한 동등성 비교 알고리즘처럼 true 를 반환한다.
이 점에서 Object.is() 메서드와는 다르다.

Array.prototype.flat()

  • 다차원 배열을 flat 하게 만든다.
  • 배열을 평평하게 만들기 위해 사용했던 concat() 메서드의 사용을 대체한다.
const original = [[1, 2, 3], 4, 5, [6, 7, 8]];
const flattened = original.flat();
console.log(flattened); // (8) [1, 2, 3, 4, 5, 6, 7, 8]

depth 조정

  • 얼마나 깊은 depth 까지 flatten 을 진행할 것인지 인수로 설정 가능하다.
  • 최대 값은 Infinity 이다.
const original = [
  [1, 2, 3],
  [[4, 5, 6], [[7, 8, 9]]],
];

const flat1 = original.flat();
console.log(flat1); // (5) [1, 2, 3, Array(3), Array(1)]
const flat2 = original.flat(2);
console.log(flat2); // (7) [1, 2, 3, 4, 5, 6, Array(3)]
const flatInfinity = original.flat(Infinity);
console.log(flatInfinity); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

Array.prototype.flatMap()

  • map 이후 flat 을 수행하는 메서드라고 보면 된다.
  • 단일 레벨만 평면화가 가능하다.
const original = [[1, 2, 3], 4, 5, [6, 7, 8]];

const flatMap = original.flatMap((e) => (typeof e === "number" ? e + 10 : e));
console.log(flatMap); // (8) [1, 2, 3, 14, 15, 6, 7, 8]
반응형