Box.bind(cat)
예전에 명시적 바인딩에 대해 공부하면서 bind에 대해 알게되었는데 제법 신기한 활용법도 있어서 따로 정리해보고 싶었다. 또 call, apply는 ES6의 spread 연산자 이후에도 계속 쓰이는지 궁금했다.
이전 포스팅에서 살펴봤던 것처럼 자바스크립트의 this
는 함수가 실행되는 시점에 호출의 주체에 따라 달라진다. 이 때 function 프로토타입의 메서드인 call
과 apply
는 this를 명시적으로 바인딩해서 함수를 바로 호출하는 기능을 한다.
두 함수는 인수를 두 종류 받는다.
첫 번째 인수로는 바인딩할 객체를 받아 대상 함수의 this를 대체한다.
두 번째 인수부터는 함수에 전달할 인수를 받는다. 여기에 call과 apply의 유일한 차이점이 존재하는데 call은 전달할 인수를 call의 두 번째 이상의 인수들로 따로 받고 apply는 배열의 형태로 받는다.
// 사용법 예시
function someFunction(a, b, c) {
console.log(this.test, a, b, c);
}
const newThis = { test: "hi" };
someFunction(1, 2, 3); // undefined 1 2 3
someFunction.call(newThis, 1, 2, 3); // hi 1 2 3
someFunction.apply(newThis, [1, 2, 3]); // hi 1 2 3
콜백 함수의 경우 내부에서 따로 설정이 되어있지 않다면 this가 전역 객체를 가리킨다. 대표적으로 setTimeout
이 그런데 이 때문에 기대하던 것과는 다른 결과가 나타날 수 있다. 따라서 콜백으로 넘겨줄 때 this를 바인딩할 필요가 있을 수 있다.
function SomeComponent(name) {
this.name = name;
this.sayMyName = function () {
console.log(this.name);
}
this.delayedSayMyName = () => {
setTimeout(this.sayMyName, 1000); // 일반 콜백 함수
setTimeout(() => {
this.sayMyName.apply(this);
}, 1000); // apply로 바인딩
}
}
const Thru = new SomeComponent("Thru");
Thru.sayMyName();
// Thru
Thru.delayedSayMyName();
// undefined
// Thru
여기서 sayMyName
메서드는 일반 함수 구문으로 만들어졌다. 메서드로 호출할 땐 호출의 주체가 Thru
객체이므로 this.name
이 제대로 출력된다. 그러나 setTimeout
의 콜백함수로 전달되었을 때는 this
가 전역 객체를 가리켜 undefined
가 출력된다. apply
를 이용해서 this
를 바인딩해준 경우 Thru
가 정상적으로 출력된다.
또한 배열 기본 메서드를 유사 배열 객체에 쓰고 싶을 때 사용할 수 있다.
function giveMeArgs() {
console.log(arguments.reverse());
}
function giveMoreArgs() {
console.log(Array.prototype.reverse.apply(arguments));
}
giveMeArgs(1, 2, 3); // Uncaught TypeError: arguments.reverse is not a function
giveMoreArgs(1, 2, 3); // [Arguments] { '0': 3, '1': 2, '2': 1 }
arguments
는 진짜 배열은 아니고 순서와 길이만 가진 유사 배열 객체이기 때문에 reverse
메서드가 프로토타입에 존재하지 않는다. 대신 Array.prototype
의 reverse
메서드에서 내부 this
를 바꿔치기 해준다면 해당 기능을 사용할 수 있다.
이는 apply
만의 특징이라고 할 수 있는데 인수 전달을 배열 형태로 할 수 있는 걸 활용해서 다수의 인수를 쉽게 전달할 수 있다.
function giveMeTenVars(a, b, c, d, e, f, g, h, i, j) {
console.log(a, b, c, d, e, f, g, h, i, j);
}
const myList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
giveMeTenVars(myList[0], myList[1], myList[2], myList[3], myList[4], myList[5], ~~);
// 길다!
giveMeTenVars.apply(null, myList);
// 간편하다!
최신 문법을 아는 사람이라면 굳이 저렇게 쓸 이유가 없다고 느낄 것이다.
ES6 부터는 스프레드 연산자...
로 iterable이나 객체 요소를 풀어 사용할 수 있기 때문이다.
function giveSpreadArgs() {
console.log([...arguments].reverse());
} // 유사 배열 바꾸기
giveMeTenVars(...myList); // 인수 뿌려주기
하지만 편한 선택지가 생긴 것이라고 생각한다. 일부 상황에서는
apply
의 성능이 더 높게 나오는 경우도 있다. 물론 대부분의 상황도 아니고 어마어마한 정도도 아니니 가끔 고려해볼만 한 정도일 것 같다.
Math.max.apply(Math, arr) vs Math.max(...arr) 의 비교 (높을 수록 빠르다)
bind
는 call
처럼 인수를 받지만 함수를 호출하지 않고 바인드된 함수의 복제본을 반환한다.
위에선 apply
된 함수를 setTimeout
에 전달하기 위해 함수로 감쌌지만 bind
를 사용하면 그럴 필요가 없어진다.
setTimeout(() => {
this.sayMyName.apply(this);
}, 1000); // apply로 바인딩
setTimeout(this.sayMyName.bind(this), 1000); // bind로 바인딩
바로 실행되지 않는다는 특성으로 인해 더 특이한 활용법이 존재한다.
함수의 일부 인수만 전달한 상태로 저장할 수 있다.
function addTwo(a, b) {
console.log(a + b);
}
const addWithTen = addTwo.bind(null, 10);
addWithTen(5); // 15
이를 통해 좀 더 구체화된 함수들로 분화시킬 수 있다.
this
가 변하는 것을 막고 부분 적용 기능만 활용하겠다면 다음과 같이 구현할 수 있다.
const partial = function() {
const originalPartialArgs = arguments;
const func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
const partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
const restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
const rabbit = {
name: '토끼',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '븧븧, ')
}
rabbit.greet('입니다!'); // 븧븧, 토끼 입니다!
greet
는 partial
로 반환된 함수이고, 해당 함수에선 인수로 전달받은 함수에 실행 환경에서의 this
를 바인딩해주고 있으므로 rabbit
의 name
이 제대로 출력된다.
위 예시를 보면 알 수 있지만 bind
말고도 클로저를 이용해 구현할 수 있다.
부분 적용 함수와 비슷하지만 인수를 하나씩 순차적으로 받고 마지막 인수를 받아야만 원본 함수가 실행되는 차이점이 있다.
사실 bind
와 관련이 있다기 보다는 클로저와 관련이 크다.
function curry5(func) {
return function(a) {
return function(b) {
return function(c) {
return function(d) {
return function(e) {
return func(a,b,c,d,e);
};
};
};
};
};
};
const getMax = curry5(Math.max);
console.log(getMax(1)(11)(7)(3)(5)); //11
순차적으로 받은 인수들을 클로저를 이용해 저장하고 있다가 마지막 인수를 받으면 함수를 호출하는 방식이다. 화살표 함수를 이용하면 더 깔끔하게 나타낼 수 있다.
const curry5 = func => a => b => c => d => e => func(a,b,c,d,e);
마지막 인수를 받을 때까지 함수 실행을 미루는 것을 함수형 프로그래밍에서는 지연 실행이라고 한다. 매개 변수의 변경 빈도가 다른 경우에 유용하다.
const getImg = baseUrl => size => id => fetch(`${baseUrl}/${size}/${id}.svg`);
// url 전달
const ImgUrl = 'http://purecatamphetamine.github.io/country-flag-icons';
const getFlagImg = getImg(ImgUrl);
// size전달
const get3x2 = getImg('3x2');
//실제요청
const getFlagUS = get3x2("US");
const getFlagKR = get3x2("KR");
fetch
요청을 보낼 때 REST API를 사용할 경우 base URL은 별로 안바뀌지만 size나 id는 자주 바뀔 수 있다. 공통적인 인수를 미리 curry으로 분화하면 효율성이나 가독성 측면에서 좋다. 실제로 Redux등의 최신 라이브러리에서도 미들웨어 등에 currying을 사용하고 있다고 한다.
부분 적용 함수도 같은 역할이 가능하지만 인수를 다 받지 않아도 실행시켜버릴 수 있다는 점에서 currying 함수가 더 안전하다고 생각한다.
이렇게 유용한 정보를 공유해주셔서 감사합니다.