rest parameter를 자유자재로 사용하고, _.shffule에서 immutable 개념을 복습해본다.
구현해볼 배열 메서드
- 함수 커스텀 메서드 : once, delay
- 배열 내장 메서드 : includes, every, some
- 객체 커스텀 메서드 : extends, defaults
- 배열 커스텀 메서드 : zip, zipStrict, intersection, difference, shuffle
함수 커스텀 메서드
'use strict';
/**
* FUNCTIONS
* =========
* 클로저의 특징을 활용해 다양한 형태의 함수를 구현할 수 있다.
* 이를 활용하여 기존 함수가 여러 번 실행되면 결과가 변동되는 함수를,
* 한 번 리턴된 값만 리턴하게 하는 함수(_.once)와
* 기존 함수가 즉시 실행 되던 함수를, 일정 시간 이후에 실행되게 하는 함수(_.delay)로 만들어본다.
*
* 이는 일반적인 프로그래밍 디자인 패턴 중 데코레이터(또는 wrapper) 패턴과 유사하다.
* 데코레이터 패턴은 객체를 꾸미거나(decorate) 감싸서(wrap) 기존 객체에 기능 또는 행동을 추가한다.
* 이론: https://refactoring.guru/design-patterns/decorator
* 구현: https://www.dofactory.com/javascript/design-patterns/decorator
*/
_.once = function (func) {
// 아래 변수들은 아래 선언/리턴되는 함수 안에서 참조된다.
// 리턴되는 함수의 scope 내에 존재하므로, 리턴되는 함수를 언제 실행해도 이 변수들에 접근할 수 있다.
let result;
let alreadyCalled = false;
return function (...args) {
if (!alreadyCalled) { // alreadyCalled 값이 false면 (초기 세팅)
alreadyCalled = true;
result = func(...args); // arguments 키워드 혹은, spread operator를 사용
}
return result;
};
};
// _.delay는 입력으로 전달되는 시간(ms, 밀리초)후
// callback 함수를 함께 전달되는 (임의의 개수의) 인자와 함께 실행한다.
// 예를 들어, _.delay(func, 500, 'a', 'b')의 결과로
// '최소' 500m가 지난 이후에 func('a', 'b')가 호출된다. (비동기)
_.delay = function (func, wait, ...args) {
setTimeout(function () {
func(...args);
}, wait);
};
배열 내장 메서드
/**
* ARRAY METHODS
* =============
* 자바스크립트 내장 배열 메소드를 직접 구현해본다.
*/
// _.includes는 배열이 주어진 값을 포함하는지 확인한다.
// 일치 여부의 판단은 엄격한 동치 연산(strict equality, ===)을 사용해야 한다.
// 입력으로 전달되는 배열의 요소는 모두 primitive value라고 가정한다.
_.includes = function (arr, target) {
let result = false;
_.each(arr, function (item) {
if (item === target) {
result = true;
}
});
return result;
};
// _.every는 배열의 모든 요소가 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다.
// test(element)의 결과(return 값)가 truthy일 경우, 통과한다.
// _.each를 반드시 사용할 필요는 없다.
// iteratee가 주어지지 않을 경우, 모든 요소가 truthy인지 확인한다.
// 빈 배열을 입력받은 경우, true를 리턴한다. (공허하게 참, vacantly true)
_.every = function (arr, iteratee) {
if (iteratee === undefined) {
iteratee = _.identity; //identity는 전달인자(argument)가 무엇이든, 그대로 리턴
}
for (let i = 0; i < arr.length; i++) {
if (!iteratee(arr[i])) {
return false;
}
}
return true;
};
// _.some은 배열의 요소 중 하나라도 test 함수(iteratee)를 통과하면 true를, 그렇지 않은 경우 false를 리턴한다.
// 빈 배열을 입력받은 경우, false를 리턴한다.
// 그 외 조건은 앞서 _.every와 동일하다.
_.some = function (arr, iteratee) {
if (iteratee === undefined) {
iteratee = _.identity;
}
for (let i = 0; i < arr.length; i++) {
if (iteratee(arr[i])) {
return true; // 빠른 return
}
}
return false;
};
객체 커스텀 메서드
/**
* CUSTOM OBJECT METHODS
* =====================
* 자바스크립트 객체를 더 쉽게 다룰 수 있는 커스텀 객체 메소드를 직접 구현해본다.
*/
// _.extend는 여러 개의 객체를 입력받아, 순서대로 객체를 결합한다.
// 첫 번째 입력인 객체를 기준으로 다음 순서의 객체들의 속성을 덮어쓴다.
// 최종적으로 (속성이 추가된) 첫 번째 객체를 리턴한다. (새로운 객체 X)
// 아래 예제를 참고
// let obj1 = { key1: 'something' };
// _.extend(
// obj1,
// {
// key2: 'something new',
// key3: 'something else new'
// },
// {
// blah: 'even more stuff',
// key3: 'overwrite"
// }
// );
// console.log(Object.keys(obj1)) // --> key1, key2, key3, blah
// console.log(obj1.key3) // --> 'overwrite"
// _.extend로 전달되는 객체의 수는 정해져 있지 않다.
// spread syntax 또는 arguments 객체를 사용해야 한다.
// 함수의 시그니쳐(함수의 입력과 출력, 함수의 모양)를 적절하게 변형하여 사용한다.
// _.each를 사용해서 구현한다.
// (아래 함수 설명) ...rest는 {},{} 등을 넣은 형태로 [{},{}]로 전달
// 예를 들어 위의 예시대로라면
// _.extend = function (obj1, [{key2: 'something new', key3: 'something else new'}, {..}]) {..} 형태가 된다.
_.extend = function (base, ...rest) {
_.each(rest, function (item) { // 예) 배열의 0번째 요소(Item 즉, 객체 {key2: 'something new', key3: 'something else new',})
_.each(item, function (val, key) { // 예) 객체 {key2: 'something new', key3: 'something else new',}의 값, 키
base[key] = val; // base인 obj1에 해당 key와 val을 넣는다.
});
});
return base;
};
// _.defaults는 _.extend와 비슷하게 동작하지만, 이미 존재하는 속성(key)을 덮어쓰지 않는다.
_.defaults = function (base, ...rest) {
_.each(rest, function (item) { // 여기까지는 똑같다. (예시를 위와 동일하게 쓰겠다.)
const keys = Object.keys(base); // keys = [ obj1의 키 값이 배열 형태로 나열된다. ]
_.each(item, function (val, key) { // 똑같이 예) 객체 {key2: 'something new', key3: 'something else new',}의 값, 키
if (base[key] === undefined) { // obj1에 뒤에 나오는 객체 속 키 값이 없으면!! **
base[key] = val; // base인 obj1에 해당 key와 val을 넣는다.
}
});
});
return base;
};
배열 커스텀 메서드
/**
* CUSTOM ARRAY METHODS
* ====================
* 자바스크립트 배열을 더 쉽게 다룰 수 있는 배열 커스텀 메소드를 직접 구현해본다.
*/
// _.zip은 여러 개의 배열을 입력받아, 같은 index의 요소들을 묶어 배열로 만든다.
// 각 index 마다 하나의 배열을 만들고, 최종적으로 이 배열들을 요소로 갖는 배열을 리턴한다.
// _.zip의 입력으로 전달되는 배열이 수는 정해져 있지 않고, 각 배열의 길이는 다를 수 있다.
// 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '긴' 배열의 길이로 통일된다.
// 특정 index에 요소가 없는 경우, undefined를 사용한다.
// 반복문(for, while)을 사용할 수 있다.
// _.each, _.reduce, _.pluck 중 하나 이상을 반드시 사용하여야 한다.
// 예시
// const arr1 = ['a','b','c'];
// const arr2 = [1,2];
// const result = _.zip(arr1, arr2)
// console.log(result); // --> [['a',1], ['b',2], ['c', undefined]]
_.zip = function (...args) { // args는 배열 속 배열 형태
const maxLength = _.reduce( // reduce에 배열 속 배열들이 몽땅 들어간다. 초기값은 0이다.
args,
function (max, item) { // 초기값 0과, 현재값 비교 (첫 현재값은 예시로 치면 arr2 배열)
if (max < item.length) {
return item.length;
}
return max; // 배열 속 배열들을 다 돌면서 가장 배열의 길이가 긴 배열의 길이값 리턴
},
0
);
const result = [];
for (let i = 0; i < maxLength; i++) { // 가장 길이가 긴 배열 만큼 반복
const grouped = _.pluck(args, i); // pluck에도 배열 속 배열들을 몽땅 넣는다. 각 배열들의 i번째 요소들 모음
result.push(grouped); // 모은 것을 추출한 배열 하나를 result 배열에 넣는다.
}
return result;
};
// _.zipStrict은 _.zip과 비슷하게 동작하지만,
// 최종적으로 리턴되는 배열의 각 요소의 길이는 입력으로 전달되는 배열 중 가장 '짧은' 배열의 길이로 통일된다.
// 그 외 조건은 앞서 _.zip과 동일하다.
_.zipStrict = function (...args) {
const minLength = _.reduce(
args,
function (min, item) {
if (item.length < min) {
return item.length;
}
return min;
},
Number.MAX_SAFE_INTEGER // 해당 상수는 JS에서 안전한 최대 정수값을 나타낸다. (2^53 - 1).
);
const result = [];
for (let i = 0; i < minLength; i++) {
const grouped = _.pluck(args, i);
result.push(grouped);
}
return result;
};
// _.intersection은 여러 개의 배열을 입력받아, 교집합 배열을 리턴한다.
// 교집합 배열은 모든 배열에 공통으로 등장하는 요소들만을 요소로 갖는 배열이다.
// 교집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
// 교집합이 없는 경우 빈 배열을 리턴한다.
// 예시 (함수 설명에도 적용해본다.)
// const set1 = ['a', 'e', b', 'c'];
// const set2 = ['c', 'd', 'e'];
// const result = _.intersection(set1, set2);
// console.log(result) // --> ['e', 'c']
// // 첫 번째 배열에 'e'가 먼저 등장
_.intersection = function (base, ...rest) { // base가 되는 set1과 [set2]형태로 전달
let result = [];
_.each(base, function (bItem) { // set1 배열을 하나하나 살펴본다. 0번째 bItem은 'a'
const intersected = _.every(rest, function (arr) { // 배열 속 배열이 들어간다. arr은 set2 배열
return _.includes(arr, bItem); // set2배열 요소에 bItem이 있으면 true 반환
});
if (intersected) { // 만약에 set2배열 요소에 bItem이 있으면
result.push(bItem); // result에 넣는다. (base 기준이므로 첫 번째 배열 기준)
}
});
return result;
// ** 다른 방법 : 차례대로 두 배열의 교집합을 구하는 방법 // ??
//
// return _.reduce( // reduce에 배열 속 배열을 넣음, 초기값 base 배열
// rest,
// function (intersected, arr) { // reduce 함수에 intersected와 배열 1개가 전달됨
// return _.filter(intersected, function (ele) {
// return _.includes(arr, ele);
// });
// },
// base
// );
};
// _.difference는 여러 개의 배열을 입력받아, 차집합 배열을 리턴한다.
// 차집합 배열은 첫 번째 배열에서 차례대로 다음 배열들의 요소들을 제외한 배열이다.
// 차집합 배열의 요소들은 첫 번째 입력인 배열을 기준으로 한다.
// 차집합이 없는 경우 빈 배열을 리턴한다.
// 예시
// const set1 = ['a', 'b', 'c'];
// const set2 = ['b', 'c', 'd'];
// const result = _.difference(set1, set2);
// console.log(result) // --> ['a']
_.difference = function (base, ...rest) { // ...rest는 배열 속 배열들 형태
let result = [];
_.each(base, function (bItem) { // set1의 0번째 'a'가 들어갈 때
const existSomewhere = _.some(rest, function (arr) { // includes에서 하나라도 true가 나오면 true 리턴
return _.includes(arr, bItem); // 0번째 배열(set2)에 'a'가 있으면 true
});
if (!existSomewhere) { // existSomewhere이 false일 때(중복 값이 하나도 없을 때)
result.push(bItem); // base의 요소를 새 배열 속에 넣어준다.
}
});
return result;
// 다른 방법 : 차례대로 두 배열의 차집합을 구하는 방법 //??
// return _.reduce(
// rest,
// function (intersected, arr) {
// return _.filter(intersected, function (ele) {
// return !_.includes(arr, ele);
// });
// },
// base
// );
};
Array.prototype.sort 요약
compareFunction이 제공되면 배열 요소는 compare 함수의 반환 값에 따라 정렬된다. a와 b가 비교되는 두 요소라면,
- compareFunction(a, b)이 0보다 작은 경우a를 b보다 낮은 색인으로 정렬한다. 즉, a가 먼저온다.
-> (음수면 첫 번째 인자부터 정렬) - compareFunction(a, b)이 0을 반환하면 보통 a와 b를 서로에 대해 변경하지 않고 모든 다른 요소에 대해 정렬한다.
- compareFunction(a, b)이 0보다 큰 경우, b를 a보다 낮은 인덱스로 소트한다.
예시)
var numbers = [4, 2, 5, 1, 3];
numbers.sort(function(a, b) { return a - b; }); // 4와 2를 비교하면 4-2는 양수, 2를 4보다 낮은 인덱스로 정렬
console.log(numbers); // [1, 2, 3, 4, 5]
// _.sortBy는 배열의 각 요소에 함수 transform을 적용하여 얻은 결과를 기준으로 정렬한다.
// transform이 전달되지 않은 경우, 배열의 요소 값 자체에 대한 비교 연산자의 결과를 따른다.
// 예를 들어, number 타입간 비교는 대소 비교이고 string 타입간 비교는 사전식(lexical) 비교이다.
// 세 번째 인자인 order는 정렬의 방향을 나타낸다. 생략되거나 1을 입력받은 경우 오름차순, -1을 입력받은 경우 내림차순으로 정렬한다.
// 예시
// const people = [
// { id: 1, age: 27 },
// { id: 2, age: 24 },
// { id: 3, age: 26 },
// ];
// function byAge(obj) {
// return obj.age;
// };
// const result = _.sortBy(people, byAge);
// console.log(result); // --> [{ id: 2, age: 24 }, { id: 3, age: 26 }, { id: 1, age: 27 }]
// 한편, 'undefined'는 비교 연산은 가능하지만 사실 비교가 불가능한(비교의 의미가 없는) 데이터이다.
// 예시
// console.log(undefined > 0); // --> false
// console.log(undefined < 0); // --> false
// console.log(undefined == 0); // --> false
// console.log(undefined === 0); // --> false
// console.log(undefined > 'hello'); // --> false
// console.log(undefined < 'hello'); // --> false
// 이러한 이유로 정렬하려는 데이터들 중 'undefined'가 있는 경우,
// 1) 'undefined' 값을 제외(filter)하고 비교하거나
// 2) 'undefined' 값을 어떤 다른 값으로 간주하여 비교해야 한다. ** 이 방식으로 적용
// 힌트
// 1. Array.prototype.sort를 사용할 수 있다.
// 참고 문서: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
// (예제를 통해 내장 메소드 sort의 특성을 꼭 확인)
// 2. _.identity를 사용할 수 있다.
// 학습 우선순위: bubble sort, insertion sort, quick sort, merge sort, radix sort
_.sortBy = function (arr, transform, order) {
order = order || 1; // 입력받은 값이 없으면 오름차순(1)
transform = transform || _.identity; // 값을 변형할 함수 입력이 없으면 값 자체 비교
const arrCloned = _.slice(arr);
return arrCloned.sort(function (a, b) { // a와 b를 비교했을 때
if (transform(a) < transform(b)) { // 콜백 함수로 값이 변형된 값이 a가 작으면
return -1 * order; // order 값 음수 or 양수로 바꿈
} // 만약 order이 -1로 큰 수부터 정렬된다면 +1로 바뀐 후 a보다 큰 수인 b가 먼저 오게 된다.
return order; // 그게 아니면 order 값 그대로
});
};
// _.shuffle은 배열 요소의 순서가 랜덤하게 변경된 새로운 배열을 리턴한다.
// 다양한 상황(예. 비디오 또는 음악 재생의 순서를 섞을 때)에서 유용하게 쓰일 수 있다.
// _.shuffle의 동작을 이해하는 것이 목적이므로, 구현할 필요는 없다.
// 아래 사이트에서 테스트 해볼 수 있다.
// https://bost.ocks.org/mike/shuffle/compare.html
// (단, 해당 사이트의 shuffle 함수는 입력으로 전달되는 array의 요소들의 위치를 '직접' 변경해야 한다.)
_.shuffle = function (arr) {
let arrCloned = arr.slice();
for (let fromIdx = 0; fromIdx < arr.length; fromIdx++) {
const toIdx = Math.floor(Math.random() * arr.length); // 배열 길이가 6이면 5까지의 랜덤 값을 뱉는다
// 아래 코드는 두 변수의 값을 교환한다.
let temp = arrCloned[fromIdx]; // c = a (a 값을 c에 임시로 할당)
arrCloned[fromIdx] = arrCloned[toIdx]; // a = b (b 값을 a에 주고)
arrCloned[toIdx] = temp; // b = c (c에 담긴 a 값을 b에게 준다)
}
return arrCloned;
};
오키! 일주일 전의 나와 다르다! 그때는 막막했는데 지금은 다 이해가 된다!!!!
역시 어제보다는 오늘이, 조금씩 성장해 가는 구나 🤍
728x90
'FE > JavaScript' 카테고리의 다른 글
[JS] fetch를 이용한 네트워크 요청 - 2/2 (활용) (0) | 2023.03.27 |
---|---|
[JS] Node.js 내장 모듈인 fs 모듈로 비동기 실습 - 2/2 (활용) (0) | 2023.03.26 |
[JS] fetch를 이용한 네트워크 요청 - 1/2 (개념) (0) | 2023.03.21 |
[JS] Node.js 내장 모듈인 fs 모듈로 비동기 실습 - 1/2 (개념) (0) | 2023.03.21 |
[JS] 동기, 비동기를 구현한 간단한 예제 (0) | 2023.03.21 |