August 16th 2018
안녕하세요.
Underscores.js의 기능들을 underscore.js라이브러리를 사용하지 않고 직접 자바스크립트 코드로 구현해보는 연습을 하고 있습니다.
동일한 기능의 함수이지만, 작성하는 사람에 따라 코드가 정말 천차만별인것을 느끼고 있습니다.
아직 어떠한 코드가 더 좋은 코드인지 확실하게 느껴지지 않지만, 이 능력을 연습을 통해 게속 늘려야겠다고 생각했습니다.
underscore.js의 소개 및 기능에 대해서는 아래의 별도 포스팅을 참고해 주세요. https://chanwhekim.github.io/2018/08/16/underscorejs/
목차 1 ) _.last 2 ) _.each 3 ) _.indexOf 4 ) _.filter 5 ) _.reject 6 ) _.uniq 7 ) _.map 8 ) _.pluck 9 ) _.reduce
1) _.last 먼저 동일한 기능을 구현하기 위해 last함수의 조건을 살펴보겠습니다. - Syntax: _.last(배열, [숫자]) - 배열의 마지막 요소를 반환함. - 두 번째 인자 만큼의 요소를 배열의 뒤쪽에서 취해 새로운 배열을 반홤함. - 두 번째 인자가 0이면, 빈 배열을 반환함. - 두 번째 인자가 배열의 길이보다 크면, 배열 전체를 반환함.
간단한 함수이지만, 처음 코드를 작성했을 때 아래와 같이 작성을 했습니다.
// _.last 구현해보기
function last_1(array, n) {
if(n === undefined) {
return array[array.length - 1];
} else {
var newArray = [];
for(var i = 1; i <= n && i <= array.length; i++) {
newArray.unshift(array[array.length - i])
}
return newArray;
}
};
↓↓↓
// 위에서 for문을 사용했는데 장황한 느낌이 있어, 다시 수정했습니다..;;
function last_1(array, n) {
if(n === undefined) {
return array[array.length - 1];
} else {
return n === 0? []: array.slice(-n);
}
};
실제 underscore.js의 last 함수에 대한 소스코드는 아래와 같습니다. Slice메서드와 Math.max 메서드가 함께 사용된 점이 좀 새로웠습니다.
// Underscore.js 소스코드
_.last = function(array, n, guard) {
if (array == null) return void 0;
if (n != null && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
2) _.each 먼저 동일한 기능을 구현하기 위해 each함수의 조건을 살펴보겠습니다. - Syntax : _.each(list, callback, [context]) - Return 값이 없어야 함. - callback 함수가 배열의 각 value, index, 원 배열에 접근 가능해야함. - 배열의 numeric key의 값에만 함수가 적용 되어야 함. - callback 함수가 객체의 value, key, 원 객체에 접근 가능해야 함.
위의 정의를 기준으로, 아래와 같이 each함수를 작성해 봤습니다.
// _.each 구현해보기
_.each = function(collection, iteratee) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iteratee(collection[i], i, collection);
}
} else {
for (var key in collection) {
iteratee(collection[key], key, collection);
}
}
};
3) _.indexOf indexOf는 동일한 기능의 익숙한 javascript 내장 함수가 있지만, underscore.js에도 포함되어 있습니다. 가까운 친구일수록 소중히 하자는 마음으로 한 번 구현해 보았습니다. - Syntax : _.indexOf(배열, value) - Return 값이 없어야 함. - 주어진 값이 배열에 있으면, 배열의 첫번째 요소의 index를 반환함. - 주어진 값이 배열에 없으면, -1을 반환함.
// _.indexOf 구현해보기
_.indexOf = function(array, target) {
var indexValue = -1;
for(var i = 0; i < array.length; i++) {
if(array[i] === target) {
return indexValue = i;
}
}
return indexValue;
}
// 위와 같이 일반적인 for loop을 통해 indexOf를 구현할 수 있습니다.
// 밑의 코드는 위의 2번에서 작성한 _.each 함수를 이용해 보았습니다.
-.indexOf = function(array, target) {
var indexValue = -1;
_.each(array, functioin(current, index) {
if(current === target) {
return indexValue = index;
}
})
}
4) _.filter filter 함수 역시 자바스크립트의 Array method로 우리에게 익숙한 분입니다. 하지만 underscore.js는 배열과 객체를 첫 인자로 받습니다. - Syntax : _.filter(list, callback, [context]); - 테스트 함수인 callback 함수의 조건에 부합하는 값만 취해서 새로운 배열을 반환함. - immutable
// _.filter 구현해보기
_.filter(list, testFunc) {
var newArray = [];
_.each(list, function(cur, x) {
if(testFunc(cur, x)) {
newArray.push(cur);
}
})
return newArray;
}
5) _.reject reject 함수는 위의 filter 함수의 반대 개념입니다. 테스트 함수에 부합하지 않는 요소들만 취해, 새로운 배열을 반환합니다. 위의 filter함수의 내부를 조금 조정해서 구현할 수도 있지만, 코드 변경없이 그대로 사용한 후 reject함수를 구현해 봤습니다.
_.reject(list, testFunc) {
var newArray = [];
var doNotInclude = _.filter(list, testFunc);
_.each(list, function(cur) {
if(!doNotInclude.includes(cur)) {
newArray.push(cur);
}
});
return newArray;
}
6) _.uniq uniq 함수는 배열에 동일한 요소가 중복되면, 중복된 요소를 제거하고 한 개의 요소만 남긴 후 새 배열을 리턴합니다.
_.uniq = function(array) {
return array.reduce(function(acc, cur) {
if (acc.indexOf(cur) < 0) {
acc.push(cur);
}
return acc;
}, []);
};
//위의 _.each method를 사용하여 작성한 uniq
_.uniq = function(array) {
var newArr = [];
_.each(array, function(cur, index, context) {
if (!newArr.includes(cur)) {
newArr.push(cur);
}
});
return newArr;
};
7) _.map _.map은 자바스크립트의 내장 메소드 map()과 그 기능은 동일합니다. 배열을 인수로 전달 받아, callback 함수의 return 값들을 배열 요소로 한 새로운 배열을 생성합니다. 하지만 underscore의 _.map()은 배열 또는 객체를 인수로 받습니다.
위의 2번에서 구현한 _.each method와 구현하는 방식이 크게 다른것이 없습니다. map method에 인자로 전달 받은 callback함수에, 첫 번째 인자로 전달 받은 배열 또는 객체의 각 요소와 인덱스를 인자롤 전달하여 실행해 주면 됩니다.
대신 _.map method의 return 값은 callback 함수의 return 값을 모아 취한 새로운 배열이 됩니다.
_.map = function(collection, callback) {
var newArr = new Array();
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
newArr.push(callback(collection[i], i));
}
} else {
for (var key in collection) {
newArr.push(callback(collection[key], key));
}
}
return newArr;
};
// 아래는 _.each()를 사용해 _.map() 작성해 보았습니다.
_.map = function(collection, callback) {
var newArr = [];
_.each(collection, function(current, indexOrKey) {
newArr.push(callback(current, indexOrKey));
});
return newArr;
};
8) _.pluck .pluck은 객체를 요소로 가지고 있는 배열에서, 객체의 특정한 key의 값만을 모아 새로운 배열을 만드는 데 편하게 사용할 수 있습니다. 위의 _.map method를 이용하면 \.pluck을 쉽게 구현할 수 있습니다.
_.pluck = function(list, propertyName) {
return _.map(list, function(current, index) {
return current[propertyName];
});
};
// _.map method를 사용하지 않고 구현해 보았습니다.
_.pluck = function(list, propertyName) {
var newArr = [];
if (Array.isArray(list)) {
for (var i = 0; i < list.length; i++) {
newArr.push(list[i][propertyName]);
}
}
return newArr;
};
9) _.reduce _.reduce는 배열 요소를 순차적으로 이동하며, callback을 적용합니다. 각 요소에 적용된 callback함수의 return 값을 다음 callback에 다시 인자로 전달한 후, 최종적으로 하나의 값을 도출하는데 유용합니다. _.reduce는 배열뿐만 아니라 객체도 처리할 수 있어야 합니다.
_.reduce() 메서드는 초기값이 있을 때와 없을 때, 작동이 조금 다르게 이루어 집니다. 이 부분 때문에, 고민을 많이 했습니다. 또한, 배열은 처리를 할 수 있겠는데, _.reduce method로 어떻게 객첵를 처리해야 하는지 방법이 잘 떠오르질 않았습니다. 우선 배열을 인자로 받았을 때 reduce method를 구현했습니다.
_.reduce = function(collection, callback, accumulator) {
var result;
if (Array.isArray(collection)) {
if (accumulator === undefined) {
accumulator = collection[0];
for (var i = 1; i < collection.length; i++) {
accumulator = iterator(accumulator, collection[i], i);
}
} else {
for (var i = 0; i < collection.length; i++) {
accumulator = iterator(accumulator, collection[i], i);
}
}
return accumulator;
}
};
_.reduce 메서드로 배열 뿐만 아니라 객체까지 처리하기 위해, 저는 객체의 key들만 따로 모아 배열을 생성했습니다. key의 배열을 생성하기 위해서 Object.keys() 메서드를 사용했습니다. Object.key()를 사용하지 않고는 for..in 문을 사용해서 배열을 사용해도 될 것 같습니다.
_.reduce = function(collection, callback, accumulator) {
else {
var keyList = Object.keys(collection);
if(accumulator === undefined) {
accumulator = collection[keyList[0]];
for(var i = 1; i < keyList.length; i++) {
accumulator = callback(accumulator, collection[keyList[i]], i);
}
} else {
for(var i = 0; i < keyList.length; i++) {
accumulator = callback(accumulator, collection[keyList[i]], i);
}
}
return accumulator;
}
}
10) _.contains .contains는 자바스크립트의 include와 동일하게 찾는 요소가 list에 있으면 true, 없으면 false를 반환합니다. 하지만 배열 또는 객체를 인수로 받습니다. 실제 underscore.js 소스 파일에는 Array.indexOf 메서드를 이용해 구현되어 있습니다. 밑에 코드는 위에서 작성한 _.reduce method를 이용해 \.contains를 구현한 코드입니다.
_.contains = function(list, target) {
_.reduce(
collection,
function(wasFound, item) {
if (wasFound) {
return true;
} else {
return item === target;
}
},
false
);
};
추가로 실제 underscore.js의 소스코드에는 아래와 같이 구현되어 있습니다.
_.contains = _.include = function(obj, target) {
if (obj === null) {
return false;
}
if (nativeIndexOf && obj.indexOf === nativeIndecOf) {
return obj.IndexOf(target) !== -1;
}
// 여기서 nativeIndexOf는 변수이며, 값은 Array.prototype.indexOf 입니다.
return any(obj, function(value) {
return value === target;
});
};
11) _.contains 12) _.every 13) _.some 14) _.extend 15) _.default
16) _.once 함수를 단 한번만 실행시키고, 그 이후의 호출 시에는 첫 호출의 결과를 반환할 때 _.once를 사용할 수 있습니다. initialization function을 만들 때 유용하게 사용할 수 있습니다.
_.once를 구현하기 위해서는, closure 영역에 두 가지 조건을 저장해 두어야 합니다. - 첫번째는 인자로 전달 받은 함수의 결과값을 저장할 변수 - 두번째는 인자로 전달 받은 함수의 기존의 호출 여부를 저장할 변수
위의 두 가지 조건을 인지하면, 아래와 같이 _.once method를 작성할 수 있습니다.
_.once = function(func) {
var isFuncCalled = false;
var result;
return function(func) {
if (!isFuncCalled) {
isFuncCalled = true;
result = func.apply(this, arguments);
// apply는 함수의 인자를 배열로 전달 가능합니다.
// 따라서, apply를 이용해 func 함수의 접근 가능한 정보를 전달해 함수 호출 시 이용할 수 있습니다.
}
return result;
// 함수가 한 번 호출 후 boolean 값이 변경되기 때문에, 항상 같은 결과값을 반환해 줍니다.
};
};
_.delay는 자바스크립트의 setTimeout과 유사합니다. _.delay method에 추가 arguments가 있으면, 이것을 callback 함수에 전달하도록 코드를 구현해야 합니다.
_.delay = function(func, wait) {
// callback에 추가 인자를 전달하기 위해서, 첫 번째, 두 번째 인자를 제외한 인자를 배열로 변경해 줍니다.
var args = Array.prototype.slice.call(arguments, 2);
// apply method로 func 함수에 인자를 묶어줍니다. apply method는 즉시 실행되기 때문에 wrapper 함수를 사용해 return 값으로 지정을 해 줍니다.
var callback = function() {
return func.apply(null, args);
};
// setTimeout을 이용해 위의 callback을 정해진 시간 후 호출시킵니다.
setTimeout(callback, wait);
};