JS - Array (23/03/08)

nazzzo·2023년 3월 8일
0

1. iterable & iterator


~ES5for

const arr = [1,2,3]
for (let i=0; i<arr.length; i++)
  console.log(arr[i])
// 1, 2, 3

ES6부터 등장한 for of

for (const v of arr)
  console.log(v)
// 1, 2, 3

아주 간단한 반복문 예제입니다
코드의 결과도 같습니다

하지만 for of문은 단순히 가독성만을 위해서 축약된 것은 아닌데요

기존의 반복문과는 다른 어떠한 차이가 존재하기 때문입니다


Set() : Set 객체는 중복되지 않는 유일한 값들의 집합입니다

const set = new Set([1, 2 ,3 ,3])
console.log(set)
// Set(3) {1, 2, 3} ... 중복값이 제거된 객체 형태를 반환합니다 


for (let i=0; i< set.size; i++) console.log(set[i])
// undefined ... 접근이 불가능합니다 

for (const a of set) console.log(a)
// 1, 2, 3 ... for of문으로는 가능하네요

위 예제를 통해 for문과 달리 for of문을 사용하면
Set 객체의 요소 반복이 가능하다는 것을 알 수 있습니다

두 문법의 내부구조에는 무언가 차이가 있다는 뜻이겠죠



Symbol : ES6에서 추가된 원시 타입입니다
iterate : 순환하다, 반복하다

for in, for of, array, object

↑ 앞의 셋은 Symbol.iterator라는 메서드를 사용하고 있습니다


그리고 Array와 같이 [Symbol.iterator]()을 가진 데이터를
이터러블(iterable)이라고 하는데, 이는 반복 가능한 객체를 의미합니다

+)
Object는 내장된 Symbol.iterator 메서드가 없습니다
배열과 달리 객체는 내부적으로 순서가 없는 요소들의 집합이기 때문...

즉 'Symbol.iterator 이 없다 = 이터러블이 아니다' 라고도 할 수 있습니다


const arr = [1,2,3]

console.log(arr[Symbol.iterator])
// 함수를 반환합니다. 내부에는 next()라는 함수가 존재합니다



그런데 만약 이터러블 객체에서 이 Symbol.iterator가 사라지면 어떻게 될까요


arr[Symbol.iterator] = null
for (const v of arr) console.log(v)
// Error: arr is not iterbale

결과 에러가 발생하는 것을 확인할 수 있습니다

for of문이 작동하려면 꼭 이터레이터를 필요로 한다는 것


이터레이터(iterator)

이터레이터(iterator)는 반복 가능한(iterable) 객체에서
값을 순회(iterate)하기 위한 인터페이스를 말합니다

이터레이터는 내부에 next() 메서드를 포함하고 있습니다

  • next()는 이터러블 객체의 다음 원소를 반환하는 함수입니다

iterable / iterator protocol에 대해서

next() 메서드가 반환하는 값이 {value, done} 형태의 객체이며,
done 값이 false에서 true가 될 때까지 반복(iterate)한다는 규약입니다


for of문도 이터러블 / 이터레이터 프로토콜에 따라 구현되어 있습니다
내장된 next() 메서드를 호출해서 {value, done} 형태의 객체를 얻고,
이 객체의 value 프로퍼티 값을 변수에 할당해서 done 값이 true가 될 때까지 반복합니다

그러니까 for of문의 반복은 위와 같은 로직을 따른다는 것입니다


Nodelist에 대해서

querySelectorAll 메서드를 사용해서 선택한 요소들은 NodeList 객체로 반환됩니다
NodeList유사 배열 객체로, 배열과 비슷한 형태를 가지고 있으며 이터러블이지만,
배열의 메서드와 속성을 모두 가지고 있지는 않습니다

예를 들어서 forEachfor of문을 사용한 반복(iterate)은 가능하지만
다른 배열 메서드인 map, filter는 사용할 수 없는데요

이는 유사배열이 Array.prototype 메서드를 상속받지 못했기 때문이며,
NodeList가 기본적으로 수정이 불가능한(read-only) 객체이기 때문이라고 합니다

그래서 NodeList에 배열 메서드를 사용하려면 스프레드 연산자나,
Array.from 메서드를 활용해서 유사배열 객체를 배열로 변환해야 합니다



스프레드 연산자에 대해서

스프레드 연산자는 배열의 모든 요소를 개별적인 요소로 분리하여 전개합니다
그런데 이 때, 전개된 개별 요소들은 새로운 배열로 복사되며 원본 배열과는 별개의 배열이 생성됩니다

[...NodeList]에 배열 메서드를 사용할 수 있는 것도 마찬가지로 원본의 요소를 추출해서
새로운 배열([], 일반적인 자바스크립트 배열) 안에 담았기 때문입니다


const originalArray = [1, 2, 3];
const copiedArray = [...originalArray];

copiedArray.push(4);

console.log(originalArray); // [1, 2, 3]
console.log(copiedArray); // [1, 2, 3, 4]
// 원본 배열을 '복사'합니다

+) 구조분해 할당

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

const [a, b, ...rest] = arr;

console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]



function aa(a) {
console.log(1,2)
}

const a = [1,2,3]
aa(...a) 
// 결과값은 1입니다. aa(...a) === aa(1,2,3)이기 때문...
// aa 함수는 첫번째 인자값인 a에 대응해서 첫번째 요소인 1만 전달받은 것입니다



2. map()


map함수의 원리 파악하기

const products = [
  { category: '의류', name: '바지', price: 10000 },
  { category: '의류', name: '셔츠', price: 15000 },
  { category: '식품', name: '과자', price: 2000 },
  { category: '식품', name: '음료', price: 3000 },
  { category: '가전제품', name: 'TV', price: 500000 },
];

위와 같은 배열에서 name값만 뽑아서 새로운 배열을 만들어내려면 어떻게 해야할까요


ES5

let newArr = []
for (let i=0; products.length; i++) {
	newArr.push(products[i].name)
}

newArr
// (5) ['바지', '셔츠', '과자', '음료', 'TV']

↑ 아주 단순하지만 이것이 map()의 로직입니다


ES6

const map = () => {
	let newArr = []
    for (const item of products) {
    	newArr.push(item.name)
    }
  	return newArr
}
const newArr = map()

names 
// (5) ['바지', '셔츠', '과자', '음료', 'TV']

ES6의 배열 문법을 사용하면 좀 더 직관적입니다


그러면 name 말고 다른 속성으로 재배열하려면 어떻게 해야할까요?


map() 메서드를 직접 만들어봅시다

const fn = (value) => value

let map = (fn, iter) => {
  let newArr =[]
  for (const v of iter) {
  	fn(v)
    newArr.push(fn(v))
  }
  return newArr
}

let category = map((value)=>value.category, products)
let price = map((value)=>value.price, products)

category
// (5) ['의류', '의류', '식품', '식품', '가전제품']

price
// (5) [10000, 15000, 2000, 3000, 500000]

이렇게 만든 map 함수는 두 개의 인자를 받습니다
(첫 번째 인자는 콜백 함수인 fn이며, 두 번째 인자는 배열 iter)


아마 자바스크립트에 내장된 Array.map()도 거의 같은 로직으로 이루어져있지 않을까요

products.map((value) => (value).category)


그리고 잊지 말아야 할 사실은 map() 메서드는 새로운 배열을 반환한다는 것...
이는 자바스크립트 배열 메서드가 내부적으로 for of 문을 사용하고 있기 때문이기도 합니다

(다만 Array.sort()같이 원본 배열을 직접 수정하는 예외도 있습니다)



3. filter()


filter 함수 원리 파악하기


const products = [
  { category: '의류', name: '바지', price: 10000 },
  { category: '의류', name: '셔츠', price: 15000 },
  { category: '식품', name: '과자', price: 2000 },
  { category: '식품', name: '음료', price: 3000 },
  { category: '가전제품', name: 'TV', price: 500000 },
];


let under = 15000
let result = []

for (const v of products) {
	if(v.price <= under) result.push(p)
}

console.log(result)
// (4) [{},{},{},{}] ~ price가 15000 이하인 요소들의 배열

filter() 만들어보기

let filter = (fn, iter) => {
    let newArr = []
    for (const v of iter) {
    	if (fn(v)) newArr.push(v)
    }
 	return newArr
}

// 콜백함수를 이용해서 재배열 조건을 사용자 입맛대로 지정할 수 있습니다
filter((value) => value.category === '의류', products)
// [
//   { category: '의류', name: '바지', price: 10000 },
//   { category: '의류', name: '셔츠', price: 15000 },
// ];

코드는 map() 함수를 만들 때와 거의 같습니다, 차이는 필터링을 위한 if문이 추가되었다는 것


이렇게 임의로 만든 메서드들은 앞서 살펴본 유사배열이나 객체에도 사용할 수 있기 때문에
자바스크립트가 제공하는 map(), filter()보다 강력하다고 볼 수도 있겠습니다



정리하자면

  • map() : 배열의 각 요소를 인자로 받은 함수를 실행해서, 새로운 배열을 생성한 뒤 반환합니다

  • filter() : 배열의 각 요소를 인자로 받은 함수를 실행해서,
    결과가 true인 요소들로만 이루어진 새로운 배열을 생성한 뒤 반환합니다


이터러블, 이터레이터에 대해서는 좀 더 공부가 필요할 것 같아요 + 제네레이터도...



0개의 댓글