티스토리 뷰
🔖TAG 💡Array.length, 💡Continue&Break, 💡mapVSforEach, 💡메서드체이닝, 💡배열 == 객체, 💡배열고차함수, 💡배열요소접근, 💡불변성, 💡유사배열객체📕

출처😌
자료🙄
Git: pocojang/clean-code-js (github.com)
목차
1. 과정 소개
2. 변수 다루기
3. 타입 다루기
4. 경계 다루기
5. 분기 다루기
6-1. JavaScript의 배열은 객체다
6-2. Array.length
6-3. 배열 요소에 접근하기
6-4. 유사 배열 객체
6-5. 불변성
6-6. for 문 배열 고차 함수로 리팩터링
6-7. 배열 메서드 체이닝 활용하기
6-8. map vs forEach
6-9. Continue & Break
7. 객체 다루기
8. 함수 다루기
6. 배열 다루기
6-1. JavaScript의 배열은 객체다
JS의 배열은 객체입니다.
배열의 동작방식이 객체의 동작방식과 비슷합니다.
아래의 코드를 예측해봅시다.
/**
* JavaScript의 배열은 객체다
*/
const arr = [1, 2, 3];
arr[3] = 'test';
arr['property'] = 'string value';
arr['obj'] = {};
arr[{}] = [1, 2, 3];
arr['func'] = function () {
return 'hello';
};
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 출력결과
// 1
// 2
// 3
// test
arr // (4) [1, 2, 3, 'test', property: 'string value', obj: {…}, [object Object]: Array(3), func: ƒ]
// 심지어 객체처럼 함수 호출도 가능합니다.
arr.func() // 'hello'
위의 코드는 아래와 매우 비슷한 형태 입니다.
const arr = {
arr: [1, 2, 3, 'test'],
property: 'string value',
obj: {},
'{}': [1, 2, 3],
func: function() {
return 'hello'
}
}
배열이 변수에 담길 수 있지만
배열처럼 보이는 녀석도 변수에 담길 수 있습니다.
/**
* JavaScript의 배열은 객체다
*/
const arr = [1, 2, 3];
if (arr.length) {
console.log('배열 확인');
}
if (typeof arr === 'object') {
console.log('배열 확인');
}
아래처럼 된다면 사고가 날 수 있겠죠?
string도 길이값을 구할 수 있기 때문에 9가 나오는 겁니다.
우리가 원하는 값은 3인데 말이죠.
const arr2 = '[1, 2, 3]';
if (arr2.length) {
console.log(arr2.length); // 9
}
그래서 사용자가 원하는 대로 Array라는걸 확실히 확인하는 과정이 필요합니다.
const arr = '[1, 2, 3]';
Array.isArray(arr) // false
요약
Array.isArray() 메서드를 이용해서
특정 변수가 배열인지 확인하는 과정을 거치면
훨씬 안정적인 코드작성을 할 수 있습니다.
6-2. Array.length
배열의 길이값을 명시적으로 조작가능합니다.
눈여겨 볼건, 길이를 늘려준 만큼 빈 요소를 추가하게 됩니다.
이것이 프로그래밍에 큰 문제를 야기할 수 있습니다.
📌JS에서 Array.length는 배열의 길이 보다는 배열의 마지막 인덱스에 가깝습니다.
Array.length는 절대 배열의 길이를 보장하지 않습니다.
배열의 마지막 인덱스를 알려줄 뿐입니다.
/**
* Array.length
*/
const arr = [1, 2, 3];
console.log(arr.length); // 3
arr.length = 10;
console.log(arr.length); // 10
console.log(arr) // (10) [1, 2, 3, 빈 ×7]
arr[20] = 20;
console.log(arr); // (21) [1, 2, 3, 빈 ×17, 20]
Array.length를 역이용 해보겠습니다.
prototype으로 clear메서드를 만들었습니다.
배열의 길이를 0으로 초기화 했을 뿐인데 배열이 간단히 초기화 되었습니다.
함수를 사용해서 배열의 길이를 0으로 초기화 해도 마찬가지 결과가 나옵니다.
/**
* Array.length
*/
Array.prototype.clear = function () {
this.length = 0;
};
function clearArray(array) {
array.length = 0;
return array;
}
const arr = [1, 2, 3]
arr // (3) [1, 2, 3]
arr.clear()
arr // []
// 위의 prototype.clear와 동일한 기능입니다.
clearArray(arr)
arr // []
결론
📌1. 배열의 length를 조작하는건 매우 주의해야하는 작업입니다.
📌2. 배열의 length는 배열의 길이를 나타내기보다 배열의 끝 인덱스를 알려주는 기능을 합니다.
6-3. 배열 요소에 접근하기
배열 요소란?
배열의 하나하나를 Element(요소)라고 합니다.
아래의 코드에서 걸림돌이 될 수 있는 부분은 inputs[0]과 inputs[1]입니다.
이것이 의미하는것이 무엇인지 알 수 없습니다.
/**
* 배열 요소에 접근하기
*/
function operateTime(input, operators, is) {
inputs[0].split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
inputs[1].split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
}
여기서 0 과 1 을 없애는 방법에 대해서 보겠습니다.
// 구조 분해 할당으로 배열요소를 명시적으로 쓰기
function operateTime(input, operators, is) {
const [firstInput, secondInput] = inputs;
fistInput.split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
secondInput.split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
}
// 조금 더 간단하게 보기 => 애초에 인자로 배열요소를 받기
function operateTime([firstInput, secondInput], operators, is) {
fistInput.split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
secondInput.split('').forEach((num) => {
cy.get('.digit').contains(num).click();
});
}
operateTime([1, 2], 1, 2)
두 번째 케이스를 보겠습니다.
여기서도 [0], [1], [2] 부분을 없애겠습니다.
/**
* 배열 요소에 접근하기
*/
function clickGroupButton() {
const confirmButton = document.getElementsByTagName('button')[0];
const cancelButton = document.getElementsByTagName('button')[1];
const resetButton = document.getElementsByTagName('button')[2];
// ...some code
}
아래처럼 굉장히 명시적으로 코드를 작성했습니다.
function clickGroupButton() {
const [confirmButton, cancelButton, resetButton] = document.getElementsByTagName('button');
// ...some code
}
세 번째 예시입니다.
아래 코드는 흔하게 만나는 코드들 입니다. 이 코드를 리팩토링 해보겠습니다.
/**
* 배열 요소에 접근하기
*/
function formatDate(targetDate) {
const date = targetDate.toISOString().split('T')[0];
const [year, month, day] = date.split('-');
return `${year}년 ${month}월 ${day}일`;
}
// 구조분해할당 이용
function formatDate(targetDate) {
const [date] = targetDate.toISOString().split('T');
const [year, month, day] = date.split('-');
return `${year}년 ${month}월 ${day}일`;
}
// 위의 코드 리팩토링
function head (arr){
return arr[0] ?? ''
}
function formatDate(targetDate) {
const date = head(targetDate.toISOString().split('T'));
const [year, month, day] = date.split('-');
return `${year}년 ${month}월 ${day}일`;
}
위 처럼 배열의 요소에 접근할 때 [0] 를 쓰지않고
구조분해할당으로 배열의 첫 요소를 가져오거나
배열의 첫 요소를 가져오는 헬퍼함수를 작성해서 이용하는것도 좋은 방법입니다.
이렇게 하면 명시적으로 배열의 첫 요소를 가져와서 쓰는지를 알 수 있습니다.
6-4. 유사 배열 객체
/**
* 유사 배열 객체
*/
const arrayLiskObject = {
0: 'HELLO',
1: 'WORLD',
length: '5',
};
// console.log(Array.isArray(arrayLiskObject)) // false
// 객체를 배열로 바꿔줍니다.
const arr = Array.from(arrayLiskObject)
console.log(arr) // (5) ['HELLO', 'WORLD', undefined, undefined, undefined]
arr.length // 5
console.log(Array.isArray(arr)) // true
arguments도 유사배열객체 중 하나 입니다.
함수의 인자로 가변적인 개수를 넘길 때
함수에 인자를 선언하지 않았음에도
함수 내부에서 arguments라는 유사배열 객체로 인자들을 다룰 수 있습니다.
아래의 출력 결과를 예측해보세요.
/**
* 유사 배열 객체
*/
function generatePriceList() {
return arguments.map((arg) => arg + '원');
}
generatePriceList(100, 200, 300, 400, 500, 600);
function generatePriceList() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}
generatePriceList(100, 200, 300, 400, 500, 600); // 100 200 300 400 500 600
이렇게 arguments가 for문으로 배열의 인덱스 접근법으로 요소를 가져오니
arguments가 배열인 것처럼 보입니다.
하지만 배열은 아닙니다. 유사 배열 객체 입니다.
function generatePriceList2(){
console.log(arguments) // Arguments(6) [100, 200, 300, 400, 500, 600, callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(Array.isArray(arguments)) // false
return arguments.map((arg) => arg + '원'); // Uncaught TypeError: arguments.map is not a function
}
generatePriceList2(100, 200, 300, 400, 500, 600);
유사 배열 객체인 arguments는 배열이 아니므로
고차함수 map, forEach, reduce, filter, some, every를 쓸 수 없습니다.
arguments를 배열로 바꾸면 고차함수를 사용할 수 있습니다.
function generatePriceList3() {
return Array.from(arguments).map(arg => arg + '원')
}
// 이제 동작🎉
generatePriceList3(100, 200, 300, 400, 500, 600); // (6) ['100원', '200원', '300원', '400원', '500원', '600원']
arguments의 __proto__ 부분을 보면 배열의 __proto__에서 볼수 있는 고차함수 들이 존재하지 않습니다.
즉, 고차함수를 쓸 수 없다는 것을 의미합니다.
function test() {
console.dir(arguments)
}
test()
/* 출력결과
__proto__: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: null
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/
반대로 배열의 __proto__를 보면 고차함수를 쓸 수 있도록 나열되어있습니다.
console.dir([])
/* 출력결과
__proto__: Array(0)
clear: ƒ ()
at: ƒ at()
concat: ƒ concat()
constructor: ƒ Array()
copyWithin: ƒ copyWithin()
entries: ƒ entries()
every: ƒ every()
fill: ƒ fill()
filter: ƒ filter()
find: ƒ find()
findIndex: ƒ findIndex()
findLast: ƒ findLast()
findLastIndex: ƒ findLastIndex()
flat: ƒ flat()
flatMap: ƒ flatMap()
forEach: ƒ forEach()
includes: ƒ includes()
indexOf: ƒ indexOf()
join: ƒ join()
keys: ƒ keys()
lastIndexOf: ƒ lastIndexOf()
length: 0
map: ƒ map()
pop: ƒ pop()
push: ƒ push()
reduce: ƒ reduce()
reduceRight: ƒ reduceRight()
reverse: ƒ reverse()
shift: ƒ shift()
slice: ƒ slice()
some: ƒ some()
sort: ƒ sort()
splice: ƒ splice()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
unshift: ƒ unshift()
values: ƒ values()
Symbol(Symbol.iterator): ƒ values()
Symbol(Symbol.unscopables): {at: true, copyWithin: true, entries: true, fill: true, find: true, …}
[[Prototype]]: Object
*/
6-5. 불변성
불변성에 대해서 들어보신적 있나요?
아래의 코드를 보고 결과값을 예측할 수 있으면
originArray를 새 변수에 담았을 때
새 변수의 값 변화를 이해하고 있다는 것입니다.
원본 배열을 새 배열에 담아냈을 때
원본 배열을 조작했을 때 새 배열도 바뀌었습니다.
/**
* 불변성 (immutable)
*
* 불변성을 지키는 2가지 방법
* 1. 배열을 복사한다.
* 2. 새로운 배열을 반환하는 메서드들을 활용한다. (map, filter, slice ...)
*/
const originArray = ['123', '456', '789'];
const newArray = originArray;
originArray.push(10);
originArray.push(11);
originArray.push(12);
originArray.unshift(0);
console.log(originArray, newArray) // (7) [0, '123', '456', '789', 10, 11, 12]
나머지연산자로 간단하게 해결할 수 있습니다.
원본 배열만 수정 되었고, 새 배열은 바뀌지 않았습니다.
const originArray = ['123', '456', '789'];
const newArray = [...originArray];
originArray.push(10);
originArray.push(11);
originArray.push(12);
originArray.unshift(0);
console.log(originArray) // (7) [0, '123', '456', '789', 10, 11, 12]
console.log(newArray) // (3) ['123', '456', '789']
더 자세한 내용은 스택오버플로우나 구글을 참고하시길 바랍니다.
6-6. for 문 배열 고차 함수로 리팩터링
변수 다루기에서 임시변수를 줄여야 한다고 했습니다.
아래와 같은 패턴으로 코드를 작성하시는 분들이 많습니다.
이럴 때는 배열 고차함수를 이용해서
배열문을 선언적으로 명시적으로 바꿔낼 수 있습니다.
/**
* 배열 고차 함수
*
* 1. 원화 표기
*/
const price = ['2000', '1000', '3000', '5000', '4000'];
function getWonPrice(priceList) {
let temp = [];
for (let i = 0; i < priceList.length; i++) {
temp.push(priceList[i] + '원');
}
return temp;
}
배열고차함수 map 메서드로 리팩토링 하겠습니다.
코드가 더 간결해졌습니다.
const price = ['2000', '1000', '3000', '5000', '4000'];
function getWonPrice(priceList) {
return priceList.map(price => price + '원')
}
getWonPrice(price) // (5) ['2000원', '1000원', '3000원', '5000원', '4000원']
여기서 아래와 같은 추가 요구사항이 생겼다고 가정해보겠습니다.
/**
* 배열 고차 함수
*
* 1. 원화 표기
* 2. 1000원 초과 리스트만 출력
* 3. 가격 순 정렬
*/
const price = ['2000', '1000', '3000', '5000', '4000'];
function getWonPrice(priceList) {
let temp = [];
for (let i = 0; i < priceList.length; i++) {
if (priceList[i] > 1000) {
temp.push(priceList[i] + '원');
}
}
return temp;
}
📌위의 코드를 아래처럼 고차함수로 리팩토링 했습니다.
const price = ['2000', '1000', '3000', '5000', '4000'];
const suffixWon = (price) => price + '원'
const isOverOneThousand = (price) => Number(price) > 1000
function getWonPrice(priceList) {
const isOverList = priceList.filter(isOverOneThousand) // ['2000', '3000', '5000', '4000']
return isOverList.map(suffixWon) // ['2000원', '3000원', '5000원', '4000원']
}
getWonPrice(price)
6-7. 배열 메서드 체이닝 활용하기
/**
* 배열 고차 함수 => 체이닝
*
* 1. 원화 표기
* 2. 1000원 초과 리스트만 출력
* 3. 가격 순 정렬
*/
const price = ['2000', '1000', '3000', '5000', '4000'];
function getWonPrice(priceList, orderType) {
let temp = [];
for (let i = 0; i < priceList.length; i++) { // A
if (priceList[i] > 1000) { // A
temp.push(priceList[i] + '원'); // B
}
}
if (orderType === 'ASCENDING') { // C
someAscendingSortFunc(temp);
}
if (orderType === 'DESCENDING') {
someDescendingSortFunc(temp);
}
return temp;
}
위의 코드를 아래에서 고차함수로 만들었습니다.
/**
* 배열 고차 함수 => 체이닝
*
* 1. 원화 표기
* 2. 1000원 초과 리스트만 출력
* 3. 가격 순 정렬
*/
const price = ['2000', '1000', '3000', '5000', '4000'];
const suffixWon = (price) => price + '원';
const isOverOneThousand = (price) => Number(price) > 1000;
const ascendingList = (a, b) => a - b;
function getWonPrice(priceList) {
const isOverList = priceList.filter(isOverOneThousand); // A
const sortList = isOverList.sort(ascendingList); // C
return sortList.map(suffixWon); // B
}
메서드 체이닝으로 코드를 더 깔끔하게 만들어 보겠습니다.
메서드 체이닝은 자료구조의 큐(Queue) 처럼 보입니다. (FIFO)
const price = ['2000', '1000', '3000', '5000', '4000'];
const suffixWon = (price) => price + '원';
const isOverOneThousand = (price) => Number(price) > 1000;
const ascendingList = (a, b) => a - b;
function getWonPrice(priceList) {
return priceList
.filter(isOverOneThousand) // filter 원하는 조건에 맞는 배열 리스트 만들기
.sort(ascendingList) // sort 정렬
.map(suffixWon) // map 배열 요소들을 다시 정리
}
getWonPrice(price) // (4) ['2000원', '3000원', '4000원', '5000원']
내장 메서드들을 잘 사용하면 고차함수처럼 활용할 수 있기 때문에
더욱 선언적으로 코드를 작성할 수 있습니다.
6-8. map vs forEach
차이점
1. return 유무
forEach의 반환값 : undefined,
map의 반환값 : 배열의 각 요소에 대해 실행한 callback의 결과를 모은 새로운 배열
아래에서 보듯 prices 배열을 forEach한 결과를 변수에 담았을 때 undefined가 되고,
prices 배열을 map한 결과를 변수에 담았을 때는 새로운 배열이 생성됩니다.
const prices = ['1000', '2000', '3000'];
const newPricesForEach = prices.forEach(function (price) {
return price + '원';
})
const newPricesMap = prices.map(function (price) {
return price + '원';
})
newPricesForEach // undefiend
newPricesMap // (3) ['1000원', '2000원', '3000원']
위의 결과만 보고 map 만 써야 겠다고 생각하는건 바람직 하지 않습니다.
만약 아래 처럼 외부의 함수를 가져와 실행할 때는 forEach를 쓰는게 좋습니다.
언어의 명세를 볼 때, map은 새로운 배열을 생성할 때 쓰는 메서드이고,
forEach는 배열 요소들을 루프시킬 때마다 콜백 함수를 실행하는 메서드 입니다.
언어의 명세에 맞게 map 이나 forEach를 선택해서 쓰는게 바람직합니다.
/**
* map vs forEach
*/
const prices = ['1000', '2000', '3000'];
prices.forEach((price) => console.log(price + '원'));
prices.map((price) => console.log(price + '원'));
6-9. Continue & Break
자주 실수하는 개념중에 하나가 Continue와 Break 입니다.
특정 레이블이나 문의 흐름을 제어하는 기능을 합니다.
continue는 흐름을 제어해서 첫번째로 다시 끌어올려줍니다.
아래는 오류가 뜨게 됩니다. forEach나 filter나 똑같이 오류가 뜹니다.
const orders = ['first', 'second', 'third'];
orders.forEach(function(order) {
if (order === 'second') {
break; // Uncaught SyntaxError: Illegal break statement
continue; // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statement
}
console.log(order);
})
이럴 때는 우선 try catch문을 써줍니다.
continue와 break를 써서 흐름을 더 잘 제어 하고 싶을 땐
어쩔 수 없이 일반적인 루프인 for / for of / for in 을 써줍니다.
const orders = ['first', 'second', 'third'];
for (let index = 0; index < orders.length; index++) {
const element = orders[index];
if (element === 'second') {
console.log('for second Break');
break;
}
}
for (const order of orders) {
if (order === 'second') {
console.log('forOf second Break');
break;
}
}
for (const key in orders) {
if (orders[key] === 'second') {
console.log('forIn second Break');
break;
}
}
또 every, some, find, findIndex메서드도 있습니다.
추가적인 설명은 여기를 참고해주세요.

참조
'WEB > JavaScript' 카테고리의 다른 글
| [클린코드 For JS] 8. 함수 다루기 (0) | 2022.06.30 |
|---|---|
| [클린코드 For JS] 7. 객체 다루기 (0) | 2022.06.30 |
| [클린코드 For JS] 5. 분기 (0) | 2022.06.28 |
| [클린코드 For JS] 4. 경계 (0) | 2022.06.27 |
| [클린코드 For JS] 3. 타입 (0) | 2022.06.27 |