1. forEach와 map
let input = [0,1,2,3,4];
input.forEach(() => { console.log("forEach. 5번 출력됨.") });
input.map(() => { console.log("map. 5번 출력됨.") });
forEach와 map은 매개변수로 콜백 함수 하나를 받는다.
그리고 배열을 순회하면서 배열의 원소들을 탐색할 때마다 (원소를 마주할 때마다) 해당 함수를 실행한다.
let input = ['a','b','c','d'];
input.forEach((x, y, z) => {
console.log("value : " + x);
console.log("index : " + y);
console.log("array : " + z);
});
input.map((x, y, z) => {
console.log("value : " + x);
console.log("index : " + y);
console.log("array : " + z);
});
forEach와 map이 실행할 콜백 함수는 세 개의 인자를 가질 수 있다.
첫 번째는 탐색한 배열 원소의 값, 두 번째는 탐색한 배열 원소의 인덱스, 세 번째는 탐색하고 있는 배열 자체를 의미한다.
그래서 로그를 확인하면 위와 같이 출력된 것을 확인할 수 있다.
여기까지는 forEach와 map이 동일한 기능을 하고 있음을 알 수 있는데, 그렇다면 차이점은 뭘까?
let input = [0,1,2,3,4];
let output1 = input.forEach(x => { return x + 3 }); // undefined
let output2 = input.map(x => { return x + 3 } ); // [3,4,5,6,7]
map과 forEach와 다른 점. map은 탐색한 배열 원소를 저장할 수 있다는 것이다.
map은 콜백 함수의 return 결과값들로 새로운 배열을 구성한 뒤 최종적으로 리턴한다.
forEach와 map 함수의 원형을 보면 바로 알 수 있다.
어떤 배열을 가공해서 새로운 배열을 리턴 받고자 한다면 map을 쓰고,
새로운 배열을 리턴받을 필요 없이, 어떤 작업을 실행하는데 배열을 참조만 하는 상황이면 forEach를 쓰면 될 것 같다.
2. for vs forEach와 map
forEach와 map을 쓰는 가장 큰 이유는 for에 비해서 코드량을 줄일 수 있고 가독성을 향상시킬 수 있기 때문일 것이다.
그리고 for문을 사용하면 순회를 시작할 인덱스, 순회 종료 조건을 정의해야 하는데 이 부분에서 실수가 나올 수 있다.
(예를 들어, i < 10과 i <= 10 에서 실수할 가능성)
다만 forEach와 map은 배열의 모든 원소를 순회할 때까지 종료되지 않으며
for문에서는 사용할 수 있었던 break와 continue를 사용할 수 없다는 제한이 있다.
3. forEach에서 어떻게 하면 순회를 중단할 수 있을까?
(1) forEach에서 return 쓰기
let input = [0,1,2,3,4];
let numbers = [];
input.forEach(x => {
if (x < 2) numbers.push(x);
else return;
});
console.log(numbers);
위의 코드를 실행하면 로그는 [0,1]이 찍히는데 예상한 대로 찍혔다고 볼 수 있다.
그렇다면 return으로 break를 완벽히 대체할 수 있을까? 정답은 아니다.
let input = [0,1,2,3,4];
let numbers = [];
input.forEach(x => {
if (x == 2) return;
numbers.push(x);
});
console.log(numbers);
위의 코드는 [0,1]이 아닌 [0,1,3,4] 로그를 출력한다.
그렇다. forEach에서의 return은 break가 아닌 continue와 같은 기능을 한다고 볼 수 있다.
(2) try/catch 추가 정의하기
let input = [0,1,2,3,4];
let loopBreak = new Error('Break');
try {
input.forEach(x => {
if (x == 2) throw loopBreak;
});
}
catch(e) {
if (e != loopBreak) throw loopBreak;
}
이 방법은 정말 억지로 break를 대체하기 위해 try/catch를 쓰는 모양이고 코드 가독성에서도 좋아 보이지 않는다.
4. some
forEach에서 순회를 종료하는 방법에 대해 구글링을 하다 보면 some으로 대체할 수 있다는 얘기들도 있다.
Array 메서드 #2에서 설명할 함수라 여기서는 짧게 정리하고 가면,
some은 배열을 순회하면서 특정 조건을 만족하는 원소가 존재하는지 확인할 수 있는 함수다.
let input = [0,1,2,3,4];
if (input.some(x => { if(x % 2 == 0) return true })){
console.log("짝수 있음")
}
보통은 이런 식으로 사용할 수 있는데, some 함수는 첫 번째 return true에서 배열의 순회가 종료되는 특징이 있다.
let input = [0,1,2,3,4];
let numbers = [];
input.some(x => {
if (x == 2) return true;
numbers.push(x);
});
console.log(numbers);
forEach에서 return을 사용하는 예시 코드에서 forEach를 some으로, return을 return true로 바꿨다.
이 코드의 결과는 성공적으로 [0,1]이 출력된다. return true가 break의 역할을 했다고 볼 수 있다.
하지만, 그렇다고 해서 forEach 대신 some을 쓰는 게 맞을까?
some이라는 함수 자체는 배열에 특정 조건을 만족하는 원소가 있는지 확인하는 함수로서의 인식이 일반적이다.
위의 코드는 numbers에 어떤 원소들이 담기는지 확인하는 것에 좀 더 포커싱이 맞춰져 있는 것 같다.
어떤 함수를 일반적이지 않은 의도로 사용하는 것도 코드 가독성을 해치는 큰 요인이다.
5. for-of
let input = [0,1,2,3,4];
let numbers = [];
for (const x of input){
if (x == 2) break;
numbers.push(x);
}
console.log(numbers);
for-of 는 ES6에 추가된 비교적 최신 문법이다. (for가 ES1, forEach가 ES5에 추가되었다)
생김새는 forEach와 비슷하면서, 그 안에서 for문처럼 break와 continue를 자유롭게 사용할 수 있다.
forEach는 세 개의 인자(원소의 값, 원소의 인덱스, 배열 전체)를 이용해 콜백 함수를 정의할 수 있었는데, for-of 에서도 가능하다.
let input = ['a','b','c','d'];
for (const x of input) console.log(x); // a,b,c,d
for (const x of input.keys()) console.log(x); // 0,1,2,3
for (const [key, value] of input.entries()) console.log(key, value); // 0 a, 1 b, 2 c, 3 d
for-of 문법은 위 처럼 사용할 수 있다.
단점이라면 타입스크립트 타겟 버전이 ES6 이전이라면 위 코드를 사용할 수 없다.
tsconfig.json 파일을 열어서 compilerOptions에 "downlevelIteration": true를 추가해줘야한다.
알다시피 타입스크립트에서 작성한 코드는 컴파일시 자바스크립트로 변환이 되는데,
forEach, map, some 등의 함수는 거의 원문 그대로 변환이 되는 반면에
위의 for-of 코드는 굉장히 많은 양의 코드로 변환이 된다.
var input = ['a', 'b', 'c', 'd'];
try {
for (var input_1 = __values(input), input_1_1 = input_1.next(); !input_1_1.done; input_1_1 = input_1.next()) {
var x = input_1_1.value;
console.log(x);
} // a,b,c,d
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (input_1_1 && !input_1_1.done && (_a = input_1.return)) _a.call(input_1);
}
finally { if (e_1) throw e_1.error; }
}
try {
for (var _d = __values(input.keys()), _e = _d.next(); !_e.done; _e = _d.next()) {
var x = _e.value;
console.log(x);
} // 0,1,2,3
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
}
finally { if (e_2) throw e_2.error; }
}
try {
for (var _f = __values(input.entries()), _g = _f.next(); !_g.done; _g = _f.next()) {
var _h = __read(_g.value, 2), key = _h[0], value = _h[1];
console.log(key, value);
} // 0 a, 1 b, 2 c, 3 d
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_g && !_g.done && (_c = _f.return)) _c.call(_f);
}
finally { if (e_3) throw e_3.error; }
}
실제로 확인해보면 타입스크립트에서 작성한 위의 4줄의 코드가 자바스크립트로 위와 같이 변환된다.
(끔찍)
결론은, forEach와 map이 아무리 for에 비해 코드를 짧게 쓸 수 있고 가독성이 좋더라도
for문의 강력한 break, continue 기능을 대체하기 어렵기 때문에 break와 continue를 사용해야 하는 상황에는 for문을 쓰는 것이 적절하다.
'JavaScript 기본' 카테고리의 다른 글
Array 함수 #5. sort (0) | 2022.03.03 |
---|---|
Array 함수 #4. reduce vs.forEach (0) | 2022.03.02 |
Array 함수 #3. 배열에 원소 추가,삭제 (2) | 2022.02.25 |
Array 함수 #2. 조건 확인 (0) | 2022.02.25 |
enum 클래스 다루기 (0) | 2021.06.16 |