underbar 함수(1)

Verba volant, scripta manent·2021년 1월 16일
1

JavaScript

목록 보기
18/20
post-thumbnail

underbar 함수

_.identity

_.identity = function (val) {
  return val;
};

전달인자(argument)가 무엇이든, 그대로 리턴하며 underbar의 기능 구현 및 테스트를 위해 재사용되는 함수이다.

collection이란?

collection은 영어로 '모음, 무리'라는 뜻이다.
컴퓨터 과학에서는 '데이터(data, 자료)의 모음'으로 부를 수 있다.

  • collection의 종류는 다양하게 있지만 배열과 객체가 대표적이다.
  • 배열 : 데이터들(요소, element)을 '순서대로' 모은 자료 구조로 요소의 위치를 통해 데이터에 접근할 수 있다.
  • 객체 : 서로 관련있는 데이터들(속성, property)을 'key-value' 형태로 '순서 없이' 모은 자료 구조로 속성에 부여된 키(key)를 통해 데이터에 접근할 수 있다.
  • collection의 각 데이터를 다루는 것은 매우 흔한 작업 중에 하나이다.
  • ex)
  1. 배열의 요소 중 가장 큰 값을 찾는 것
  2. 배열의 모든 요소의 합을 구하는 것
  3. 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것
  4. 각 작업들은 collection의 각 데이터를 가지고 비슷한 처리를 한다.
  5. 배열의 요소 중 가장 큰 값을 찾는 것 => 현재 데이터가 가장 큰 값인지 확인
  6. 배열의 모든 요소의 합을 구하는 것 => 현재 데이터를 누적값에 더하기
  7. 객체에 특정 속성의 이름(key)이 존재하는지 확인하는 것 => 현재 key가 원하는 값인지 확인

_.slice

_.slice = function (arr, start, end) {
  // 변수를 선언할 경우, 아래와 같이 콤마(,)를 이용해 선언할 수 있다.
  // 이때, 콤마로 연결된 변수들은 모두 동일한 선언 키워드(let, const)가 적용된다.
  let _start = start || 0, // `start`가 undefined인 경우, slice는 0부터 동작한다.
    _end = end; // 입력받은 인덱스가 음수일 경우, 마지막 인덱스부터 매칭한다. 
    (예. -1 => arr.length - 1, -2 => arr.length - 2)
  // 입력받은 인덱스는 0 이상이어야 한다.
  if (start < 0) _start = Math.max(0, arr.length + start);
  if (end < 0) _end = Math.max(0, arr.length + end);

  // `end`가 생략될 경우(undefined), slice는 마지막 인덱스까지 동작한다.
  // `end`가 배열의 범위를 벗어날 경우, slice는 마지막 인덱스까지 동작한다.
  if (_end === undefined || _end > arr.length) _end = arr.length;

  let result = [];
  // `start`가 배열의 범위를 벗어날 경우, 빈 배열을 리턴한다.
  for (let i = _start; i < _end; i++) {
    result.push(arr[i]);
  }

  return result;
};

_.take

_.take = function (arr, n) {
  let result = [];
  if(n >= arr.length) {
    return arr;
  } // n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
  for(let i = 0; i < n; i++) {
    if(n === undefined || n < 0) { // n이 undefined이거나 음수인 경우, 빈 배열을 리턴한다.
      return result;
    } else {
      result.push(arr[i]); // n이 배열의 길이 안에 있으면 arr[i]를 빈 배열에 추가한다.
    }
  }
  return result;
};

배열의 처음 n개의 element를 담은 새로운 배열을 리턴한다.

_.drop

_.drop = function (arr, n) {
  let result = [];
  if(n >= arr.length) {
    return result;
  } // n이 배열의 길이를 벗어날 경우, 빈 배열을 리턴한다.
  for(let i = 0; i < arr.length; i++) {
    if(n === undefined || n < 0) { // n이 undefined이거나 음수인 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴한다.
      result.push(arr[i]);
    } else if(i >= n){
      result.push(arr[i]); // n이 배열의 길이 안에 있으면 arr[i]를 빈 배열에 추가한다.
    }
  }
  return result;
};

처음 n개의 element를 제외한 새로운 배열을 리턴한다.

_.last

_.last = function (arr, n) {
  let result = [];
  if(n === undefined || n < 0) { // n이 undefined이거나 음수인 경우, 배열의 마지막 요소만을 담은 배열을 리턴한다.
    result.push(arr[arr.length-1]);
    return result;
  } 
  for(let i = 0; i < arr.length; i++) {
    if(n >= arr.length) { // n이 배열의 길이를 벗어날 경우, 전체 배열을 shallow copy한 새로운 배열을 리턴힌다.
      return arr;
    } else { // n의 범위인 경우 배열의 길이에서 n을 뺀 위치의 길이에서 배열의 길이까지 리턴한다.
      return _.slice(arr,arr.length -n,arr.length);
    }
  }
  return result;
};

_.last = function(arr, n) {
  if(n === undefined) {
    return array[arr.length-1];
    } else if(arr.length <= n) {
      return arr;
    } else{
      return arr.slice(arr.length-n, arr.length);
    }
};

배열의 마지막 n개의 element를 담은 새로운 배열을 리턴한다.

_.each

_.each = function (collection, iteratee) {
  if(Array.isArray(collection)) { // collection이 배열이면
    for(let i = 0; i < collection.length; i++) {
      iteratee(collection[i], i, collection);    
    }  // collection[i]: element , i: index , collenction: array
  } else if(typeof collection === 'object'){ // collection의 타입이 객체면
    for(let key in collection) {
      iteratee(collection[key], key ,collection);
    } // collection[key]: value , key: key , collection : object
  }
};

collection의 각 데이터에 반복적인 작업을 수행한다.

_.each에 대한 보충설명

  1. collection(배열 혹은 객체)과 함수 iteratee(반복되는 작업)를 인자로 전달받아 (iteratee는 함수의 인자로 전달되는 함수이므로 callback 함수)
  2. collection의 데이터(element 또는 property)를 순회하면서
  3. iteratee에 각 데이터를 인자로 전달하여 실행한다.
    iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 한다.
  • 배열 arr을 입력받을 경우, iteratee(ele, idx, arr)
  • 객체 obj를 입력받을 경우, iteratee(val, key, obj)
  • 이처럼 collection의 모든 정보가 iteratee의 인자로 잘 전달되어야 모든 경우를 다룰 수 있다.
  • 실제로 전달되는 callback 함수는 collection의 모든 정보가 필요하지 않을 수도 있다.

_.indexOf

_.indexOf = function (arr, target) {
  let result = -1;

  _.each(arr, function (item, index) {
    if (item === target && result === -1) { // result가 -1이고, 
    요소와 target이 일치하면 result에 해당 index를 할당한다.
      result = index;
    }
  });

  return result;
};
// 여기서 `result === -1`을 조건에 넣어준 이유는 target과 중복인 item이 여러개일때 
첫 중복 item의 index를 result에 할당해주면, 
이후 중복인 item이 조건문에 들어갔을 때 result === -1이 성립되지 않기 때문에 
result 값은 첫 중복 item의 index 그대로 리턴되기 때문이다.
for(let i=0; i < array.length; i++){
      if(array[i] === target){
        return i;
      }
    }
    return -1;

target으로 전달되는 값이 배열에서 발견되면, 그 index를 반환하고, 만일 배열에서 발견할 수 없다면 -1을 반환한다.
target이 중복해서 존재하는 경우, 가장 낮은 index를 리턴한다.

_.filter

_.filter = function (arr, test) {
  let result = [];
  _.each(arr, function(el) {
    if(test(el)) { // test(element)의 결과(return 값)가 truthy일 경우 통과하며 test 함수는 각 요소에 반복 적용된다.
      result.push(el);
    }
  });
  return result;
};
_.filter = function(arr, test) {
    let result = [];
    for(let i=0; i < arr.length; i++){
      if(test(arr[i])){
        result.push(arr[i]);
      }
    }
    return result;
  };

test 함수를 통과하는 모든 요소를 담은 새로운 배열을 리턴한다.

_.reject

_.reject = function (arr, test) {
  let result = [];
  _.each(arr, function(el) {
    if(!test(el)) { // test(element)의 결과(return 값)가 falsy일 경우 통과하며 test 함수는 각 요소에 반복 적용된다.
      result.push(el);
    }
  });
  return result;
};
_.reject = function(arr, test) {
    let result = [];
    for(let i=0; i < arr.length; i++){
      if(!test(arr[i])){
        result.push(carr[i]);
      }
    }
    return result;
  };
_.reject = function(arr, test) {
    return _.filter(arr, function(el){
      return !test(el);
    });
  };

test 함수를 통과하지 않는 모든 요소를 담은 새로운 배열을 리턴한다. (_.filter와 반대)

_.uniq

_.uniq = function (arr) {
  let result = [];

  _.each(arr, function(element) {            
    for (let i = 0; i < arr.length; i++) {   
      if (result[i] === element) {
        return false; // for문안에서 result[i]가 element와 같을때 false로 리턴해서 for문 밖으로 넘어가는것을 막아준다.
      }
    }
    result.push(element); // each와 for문안에서 중복되지 않은 요소들 push.
  });
  return result;
};
_.uniq = function(arr) {
  let result = [];
    
  for(let i=0; i < arr.length; i++){
    if(_.indexOf(result, arr[i]) === -1){ 
      result.push(arr[i]);
    }
  }
 return result;
};
_.uniq = function(arr) {
  let result = [];
    
  _.each(arr, function(el){
    if(_.indexOf(arr, el) === -1){
      result.push(el);
    }
  });
 return result;
};
_.uniq = function(arr) {
  let obj = {};
    
  for(let i=0; i < arr.length; i++){
    obj[arr[i]] = arr[i];
  }
 return Object.values(obj);
};

주어진 배열의 요소가 중복되지 않도록 새로운 배열을 리턴한다.

_.map

_.map = function (arr, iteratee) {
  let result = [];
  _.each(arr,function(el){
    result.push(iteratee(el))
  });
  return result;
};
_.map = function(arr, iteratee) {
    let result = [];
    for(let i=0; i < arr.length; i++){
      result.push(iteratee(arr[i]));
    }
    return result;
  };

iteratee(반복되는 작업)를 배열의 각 요소에 적용(apply)한 결과를 담은 새로운 배열을 리턴한다.
배열의 각 요소를 다른 것(iteratee의 결과)으로 매핑(mapping)한다.

_.pluck

_.pluck = function (arr, keyOrIdx) {
  let result = [];
  _.map(arr, function (item) {
    result.push(item[keyOrIdx]);
  });
  return result;
};
_.pluck = function(arr, keyOrIdx) {
  return _.map(arr, function(item) {
    return item[keyOrIdx];
  });
};

collection의 key값이 동일한 값들만을 모아 반환한다.

_.pluck에 대한 보충설명

  1. 객체 또는 배열을 요소로 갖는 배열과 각 요소에서 찾고자 하는 key 또는 index를 입력받아
  2. 각 요소의 해당 값 또는 요소만을 추출하여 새로운 배열에 저장하고,
  3. 최종적으로 새로운 배열을 리턴한다.
    ex)
    각 개인의 정보를 담은 객체를 요소로 갖는 배열을 통해서, 모든 개인의 나이만으로 구성된 별도의 배열을 만들 수 있다.
    최종적으로 리턴되는 새로운 배열의 길이는 입력으로 전달되는 배열의 길이와 같아야 한다.
    따라서 찾고자 하는 key 또는 index를 가지고 있지 않은 요소의 경우, 추출 결과는 undefined 이다.

_.reduce

_.reduce = function (arr, iteratee, initVal) {
  let acc = initVal;
  _.each (arr, function(item, idx) {
    if(initVal === undefined && idx === 0) { // 만약 initVal이 없고 인덱스가 0번째이면 초기값을 한번만 처음에 넣어준다
      acc = item;
    }
    else {
      acc = iteratee(acc, item, idx, arr);
    }
  });
  return acc;
};
_.reduce = function (arr, iteratee, initVal) {
  let index = 0;
    
  if(initVal === undefined){
    initVal = arr[0];
    index = 1;
  }
  for(let i=index; i < arr.length; i++){
    initVal = iteratee(initVal, arr[i])
  }
  return initVal;
};
_.reduce = function(arr, iteratee, initVal) {
    let slice = Array.prototype.slice;
    
    if(initVal === undefined){
      initVal = arr[0];
      arr = slice.call(arr,1);
    }
    _.each(arr, function(el){
      initVal = iteratee(initVal, el);
    });
    return initVal
  };

입력받은 collection으로부터 iteratee의 내용이 무엇인지에 따라서 하나의 결과값을 반환한다.
accumulator는 초기값으로, 이 accumulator가 주어지느냐, 주어지지않느냐의 두가지 경우로 나뉜다.

_.reduce에 대한 보충설명

  1. 배열을 순회하며 각 요소에 iteratee 함수를 적용하고,
  2. 그 결과값을 계속해서 누적(accumulate)한다.
  3. 최종적으로 누적된 결과값을 리턴한다.

_.reduce는 위에서 구현한 많은 함수처럼, 입력으로 배열과 각 요소에 반복할 작업(iteratee)을 전달받습니다.
iteratee에 대해서 복습하면 아래와 같습니다. (일반적으로 객체를 reduce 하지는 않으므로, 배열 부분만 복습합니다.)
iteratee는 차례대로 데이터(element 또는 value), 접근자(index 또는 key), collection을 다룰 수 있어야 합니다.
배열 arr을 입력받을 경우, iteratee(ele, idx, arr)

.reduce는 반복해서 값을 누적하므로 이 누적되는 값을 관리해야 한다.
따라서
.reduce의 iteratee는 인자가 하나 더 추가되어 최종 형태는 아래와 같다.

iteratee(acc, ele, idx, arr)

누적되는 값은 보통 tally, accumulator(앞글자만 따서 acc로 표기하기도 함)로 표현하거나
목적을 더 분명하게 하기 위해 sum(합), prod(곱), total 등으로 표현하기도 한다.
이때, acc는 '이전 요소까지'의 반복 작업의 결과로 누적된 값이다.
ele는 잘 아시다시피 반복 작업을 수행할(아직 수행하지 않은) 현재의 요소이다.

여기까지 내용을 정리하면 다음과 같다.

_.reduce(arr, iteratee)
iteratee(acc, ele, idx, arr)

그런데 사실 누적값에 대해서 빠뜨린 게 하나 있다.
바로 '누적값은 어디서부터 시작하는가'라는 의문에 대한 대답을 하지 않았다.
이를 해결하는 방법은 초기 값을 직접 설정하거나 자동으로 설정하는 것이다.
_.reduce는 세 번째 인자로 초기 값을 전달받을 수 있다.
이 세 번째 인자로 초기 값이 전달되는 경우, 그 값을 누적값의 기초(acc)로 하여 배열의 '첫 번째' 요소부터 반복 작업이 수행된다.
반면 초기 값이 전달되지 않은 경우, 배열의 첫 번째 요소를 누적값의 출발로 하여 배열의 '두 번째' 요소부터 반복 작업이 수행된다.

따라서 최종적인 형태는 아래와 같다.

_.reduce(arr, iteratee, initVal)
iteratee(acc, ele, idx, arr)
profile
말은 사라지지만 기록은 남는다

0개의 댓글