[JS] call, apply, bind

thru·2023년 8월 16일
1

Box.bind(cat)

궁금했던 내용

예전에 명시적 바인딩에 대해 공부하면서 bind에 대해 알게되었는데 제법 신기한 활용법도 있어서 따로 정리해보고 싶었다. 또 call, apply는 ES6의 spread 연산자 이후에도 계속 쓰이는지 궁금했다.


call, apply

이전 포스팅에서 살펴봤던 것처럼 자바스크립트의 this는 함수가 실행되는 시점에 호출의 주체에 따라 달라진다. 이 때 function 프로토타입의 메서드인 callapply 는 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 바인딩

콜백 함수의 경우 내부에서 따로 설정이 되어있지 않다면 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.prototypereverse 메서드에서 내부 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

bindcall처럼 인수를 받지만 함수를 호출하지 않고 바인드된 함수의 복제본을 반환한다.

위에선 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('입니다!');  // 븧븧,  토끼 입니다!

greetpartial로 반환된 함수이고, 해당 함수에선 인수로 전달받은 함수에 실행 환경에서의 this를 바인딩해주고 있으므로 rabbitname이 제대로 출력된다.

위 예시를 보면 알 수 있지만 bind말고도 클로저를 이용해 구현할 수 있다.

currying 함수

부분 적용 함수와 비슷하지만 인수를 하나씩 순차적으로 받고 마지막 인수를 받아야만 원본 함수가 실행되는 차이점이 있다.

사실 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 함수가 더 안전하다고 생각한다.


참조

profile
프론트 공부 중

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

이렇게 유용한 정보를 공유해주셔서 감사합니다.

답글 달기