[Javascript] call, apply, bind에 대하여

박기영·2022년 12월 4일
0

Javascript

목록 보기
23/45

바인딩. 막히는게 있어서 질문을 올리면 꽤나 자주 등장하는 단어이다.
이 "바인딩"에 관련 메서드들이 있다. 바로 call, apply, bind이다.
모두 this의 바인딩을 변경한다는 공통점이 있는데, 어떤게 다른지 살펴보자.

call

우선 MDN 문서부터 살펴보자.

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.
- MDN docs -

사용법은 다음과 같다.

func.call(thisArg[, arg1[, arg2[, ...]]])

음...와닿지않는다. 예시를 살펴보자.

const obj = {
  name: "Rki0",
  job: "백수",
};

function callTest() {
  return this;
}

console.log(callTest());  // window 객체

callTest 함수가 반환하는 thiswindow 객체를 가리킨다.
그런데...나는 죽어도 callTest 함수를 사용해서 obj 객체를 사용하고 싶다.
call() 사용하면 이게 가능하다!

첫 번째 인자

const obj = {
  name: "Rki0",
  job: "백수",
};

function callTest() {
  return this;
}

console.log(callTest.call(obj));  // obj 객체
console.log(callTest.call(obj).name);  // Rki0

call()의 첫 번째 인자로 callTest 함수 내에서 this로 사용하고자 하는 객체를 넣어준다.
그러면 callTest 함수 내에서 this가 가리키는 것이 obj가 된다!

그런데, 사용법을 보면 인자를 여러개 넣을 수 있다.
여러 개 넣을 때는 어떤 기능이 있는걸까?

두 번째 인자 ~ ...

예시를 조금 바꿔보았다.

const obj = {
  name: "Rki0",
  job: "백수",
};

function callTest(job) {
  return `Want ${job}, but now ${this.job}`;
}

console.log(callTest("FE developer"));  // Want FE developer, but now undefined
console.log(callTest.call(obj));  // Want undefined, but now 백수
console.log(callTest.call(obj, "FE developer"));  // Want FE developer, but now 백수

callTest 함수에 문자열을 인자로 넣으면 job 파라미터에 들어가서 출력된다.
그치만 thiswindow를 가리키고 있기 때문에 this.jobundefined가 나온다.

그래서 obj 객체를 this로 가리키기 위해서 call()을 사용해 바인딩을 했다.
그 결과, this.jobobj 객체의 job을 가져올 수 있었지만,
callTest 함수 자체의 파라미터인 job에는 아무것도 들어오지 않은 undefined가 출력된다.

그래서 call()의 두 번째 인자에 인자를 넣어줬다.
이 인자는 callTest 함수의 파라미터에 해당하는 값이다.
그 결과, jobthis.job이 모두 값을 가지게 되었다.

정리

아! 이제 이해가 된다.
call()의 첫 번째 인자는 대상이 되는 함수에서 사용하고싶은 객체를,
두 번째 인자는 대상이 되는 함수에 전달할 인자를 입력하는 것이다.
물론, 함수에 따라 파라미터 개수가 다르므로 3,4,... 계속 증가할 수 있다.

마지막으로 좀 더 복잡한 예시를 하나 들고 call()은 마무리하고자 한다.

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

console.log(new Food('cheese', 5).name); // cheese

Product 함수에서 thiswindow 객체를 가리킨다.
Food 함수에서도 마찬가지 thiswindow..라고 생각했으나
new 생성자를 통해 호출되었으므로, 여기서는 thisFood에 바인딩된다.

이제 call()에 대한 부분을 살펴보자.
Food 함수 내에서 call()을 사용해 Product 함수에 this를 바인딩해줬다.
this는 방금 설명했듯이 Food에 바인딩된 this이다.
따라서, Product에서 this.name === Food.name이고, this.price === Food.price가 된다.

생성자로 Food 함수를 생성할 때, 인자로 nameprize를 전달했다.
이 인자는 Product 함수까지 전달되며,
Product 함수는 call()에 의해 Foodthis에 바인딩이 되었고,
Food 함수는 본인은 nameprize를 별도로 어딘가에 저장하지 않았음에도
Product 함수에서 그 값들을 꺼내 쓸 수 있게되었다.

정리해보자.
이 모든 것은 Product 함수가 Food 함수의 this를 바인딩하고 있기 때문이다.
Food 함수와 Product 함수가 같은 this를 사용하고 있으며,
thisFood 함수의 this인 것이다.

따라서, 아래와 같은 접근도 가능해진다.

function Product(name, price) {
  this.name = name;
  this.price = price;
  this.from = "korea";
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = "food";
}

console.log(new Food("cheese", 5).from); // korea

Product 함수와 Food 함수가 같은 this를 사용하기 때문에 접근할 수 있는 것이다.

call()을 써서 this를 정의하면 다른 객체의 파라미터나 메소드를 사용할 수 있다.

apply

우선 MDN 문서부터 살펴보자.

apply() 메서드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출합니다.
- MDN docs -

사용법은 다음과 같다.

func.apply(thisArg, [argsArray])

사용법이 call()과 크게 다른 것이 없다.

이 함수의 구문은 거의 call() 구문과 유사합니다. 근본적인 차이점은 call() 은 함수에 전달될 인수 리스트를 받는데 비해, apply() 는 인수들의 단일 배열을 받는다는 점입니다.
- MDN docs -

그렇다. 차이점은 apply()에서는 인자를 배열의 형태로 넘겨준다는 것이다.

예시를 살펴보자.
call()에서 사용한 예시를 그대로 사용하겠다.

const obj = {
  name: "Rki0",
  job: "백수",
};

function applyTest() {
  return this;
}

console.log(applyTest()); // window 객체

나는 죽어도 objapplyTest()를 통해서 접근하고싶다.
apply()를 적용해보자!

첫 번째 인자

const obj = {
  name: "Rki0",
  job: "백수",
};

function applyTest() {
  return this;
}

console.log(applyTest.apply(obj)); // obj 객체
console.log(applyTest.apply(obj).name); // Rki0

call()에서와 마찬가지로 원래는 window 객체를 가리켰던 것이,
이제는obj 객체를 가리키는 것을 볼 수 있다.
그 덕분에 name이라는 key에 접근해서 값을 가져올 수도 있다.

자, 여기까지는 call()과 똑같다.
두 번째 인자를 전달하는 방법이 다르기 때문에 어떤 방식인지 살펴보자.

두 번째 인자

예시를 약간 수정했다.

const obj = {
  name: "Rki0",
  job: "백수",
};

function applyTest(job) {
  return `Want ${job}, but now ${this.job}`;
}

console.log(applyTest("FE developer")); // Want FE developer, but now undefined
console.log(applyTest.apply(obj)); // Want undefined, but now 백수
console.log(applyTest.apply(obj, ["FE developer"])); // Want FE developer, but now 백수

apply()에서는 두 번째 인자에 대상 함수가 사용할 인자를 전달해준다.
어떻게? 배열의 형태로!
call()에서와 다른 점이 저것밖에 없어서 참...설명을 더 하기도 애매하다 😅

아! 주의할 점이 있다.
배열의 형태로 전달한다고 해서 인자가 생길 때마다 배열을 생성하는 것은 아니다.
예를 들어, 위 예시에서

function applyTest(job, age) {
  return `Want ${job}, but now ${this.job}. Age...${age}`;
}

이렇게 함수가 바뀌었다고 해보자.
파라미터가 2개로 늘었으니, 인자도 2개를 전달해야된다.
그렇다면 apply() 쪽은 다음과 같이 변한다.

console.log(applyTest.apply(obj, ["FE developer", 26]));
// Want FE developer, but now 백수. Age...26

배열은 하나만 넣어주고, 그 배열 안에 인자를 추가하는 것이다.
헷갈리지말자!

정리

정리할 겸, 추가적인 내용을 알아보자.

const numbers = [5, 6, 2, 3, 7];

const max = Math.max.apply(null, numbers);

console.log(max);  // 7

const min = Math.min.apply(null, numbers);

console.log(min);  // 2

apply()를 사용하여 this 바인딩을 하고, numbers 배열을 사용하는 모습이다.
위에서부터 공부를 했음에도 불구하고 생각보다...이해가 되지않는다.

왜? null의 존재가 그 이유라고 생각한다.
뭐, numbers가 배열이니까 배열을 직접 입력하지않고 상수 형태로 넘겨준거니 이해한다지만,
null은 도대체 왜 나온거지? this로 사용할 객체를 넣어줘야하는거 아닌가?

메소드가 non-strict mode 코드의 함수일 경우, null 과 undefined 가 전역 객체로 대체되며, 기본 값은 제한됩니다.
- MDN docs -

그렇다. 엄격 모드(strict mode)가 아닌 경우에는 null, undefined가 전역 객체를 대체한다고한다.
따라서, 위 예제에서 사용된 nullwindow 객체, 즉 전역 객체를 의미하기에
numbers에 대한 접근이 가능했던 것이다.

const로 선언되어서 혹시나 이해가 잘 안된다면

var numbers = [5, 6, 2, 3, 7];

var로 사용했을 때의 경우를 생각하면 된다. 결과는 똑같다.
어찌되었든 전역 객체에 들어있는 프로퍼티라서 접근이 가능하다는 의미이다.

추가로, null 대신 undefined로 사용해도 같은 결과가 나온다.

const max = Math.max.apply(undefined, numbers);

call, apply은 어디에 활용할까?

음...아무튼 둘 다 this의 바인딩을 자유롭게 조절할 수 있다는 것은 알게되었다.
그런데 어떤 경우에 쓰게 되는걸까?
예시를 통해 알아보자. 예시는 inpa님 블로그를 참고했다.

function callAndapply() {
  console.log(arguments);  // ["Rki0", "백수", 26]
}

callAndapply("Rki0", "백수", 26); 

일반 함수는 이런 식으로 arguments라는 것을 속성으로 가지고 있다.
예제를 보면 알 수 있듯이 arguments는 인자로 전달된 것들을 담고 있다.
배열 형태로 보이지만 실제로 배열이 아니라 유사 배열이다.
즉, 배열에 사용되는 메서드들은 사용할 수가 없다.

그치만 call(), apply()를 사용하면 이를 가능하게 할 수 있다.
아래 예시를 보자.

function callapply() {
  console.log(Array.prototype.join.call(arguments)); // "Rki0,백수,26"
  console.log(Array.prototype.join.apply(arguments)); // "Rki0,백수,26"
}

callapply("Rki0", "백수", 26);

join()이라는 배열에 사용되는 메서드에
call(), apply()를 이용해서 arguments를 바인딩을 해줬다.
원래대로라면 배열을 뜻하는 Array 객체를 가리키므로 배열이 아닌arguments를 사용할 수 없지만,
call(), apply()를 활용하여 바인딩을 해줬기 때문에 arguments도 사용할 수 있게 된 것이다.

따라서, 아래와 같은 활용이 가능하다.

const items = [1, 4];
 
items.join() // "1,4"

Array.prototype.join.call(items); // "1,4"
[].join.call(items); // "1,4"

Array.prototype.join.apply(items); // "1,4"
[].join.apply(items); // "1,4"

결국, 하고 싶은 말은 하나다.
call(), apply()this 바인딩을 통해 다른 객체의 프로퍼티를 사용할 수 있다는 것이다.

bind

우선 MDN 문서부터 살펴보자.

bind() 메소드가 호출되면 새로운 함수를 생성합니다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공됩니다.
- MDN docs -

사용법은 다음과 같다.

func.bind(thisArg[, arg1[, arg2[, ...]]])

음...call()에서와 같은 형태로 사용되는구나!
그런데...그럼 둘이 뭐가 다른거지..?
예시를 살펴보면서 알아보자.

첫 번째 인자

const obj = {
  name: "Rki0",
  job: "백수",
};

function bindTest() {
  return this;
}

console.log(bindTest.bind(obj)); // bindTest 함수

어..? 뭔가 생각했던 것과 다르다.
call(), apply()에서는 저렇게 사용하면 obj 객체가 출력이 됐는데,
bind()는 그냥 bindTest 함수가 출력된다.
즉, 함수가 호출이 안된다.

사실 이 차이는 MDN 설명을 유심히 봤다면 눈치채고 있었을 것이다.
call(), apply()는 바인딩과 동시에 함수를 호출한다.
그러나 bind()는 호출하지않고 생성까지만 한다.

따라서, 다른 두 녀석처럼 사용하고자 한다면 코드를

console.log(bindTest.bind(obj)()); // obj 객체

이렇게 사용해야한다. 별도로 호출을 해줘야한다는 것이다!

두 번째 인자 ~ ...

예시를 조금 바꿔보았다.

const obj = {
  name: "Rki0",
  job: "백수",
};

function bindTest(job) {
  return `Want ${job}, but now ${this.job}`;
}

console.log(bindTest("FE developer")); // Want FE developer, but now undefined
console.log(bindTest.bind(obj));  // bindTest 함수
console.log(bindTest.bind(obj, "FE developer"));  // bindTest 함수

첫 번째 인자 설명에서 잠깐 말했듯이 함수의 호출은 하지 않기 때문에
문자열을 출력해주지는 않는다.
call(), apply()와 동일한 결과를 만들어내고 싶다면 아래와 같이 사용해야한다.

const obj = {
  name: "Rki0",
  job: "백수",
};

function bindTest(job) {
  return `Want ${job}, but now ${this.job}`;
}

console.log(bindTest("FE developer")); // Want FE developer, but now undefined
console.log(bindTest.bind(obj)()); // Want undefined, but now 백수
console.log(bindTest.bind(obj, "FE developer")()); // Want FE developer, but now 백수

이제 명확하게 이해가 된다.

정리

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

console.log(module.getX());  // 42

const unboundGetX = module.getX;
console.log(unboundGetX());  // undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());  // 42

this는 메서드가 호출될 때 사용된 객체를 가리킨다.
따라서 unboundGetX 함수를 호출할 때는 window 객체를 가리키므로 undefined를 출력한다.

bind()를 통해 unboundGetX 함수에서 module 객체를 사용하겠다고 지정해준 뒤에는
this를 통해 module 객체 접근 가능하기 때문에 boundGetX 함수 호출 시 의도했던 값이 출력된다.

bind는 어디에 활용할까?

이 부분은 필자가 우테코 5기 프리코스에서 다른 분들과 코드 리뷰를 주고 받으며 경험한 내용이다.
MVC 패턴을 구현하기 위해 UI 로직과 핵심 로직을 분리하는 시도를 하였는데,
이 때, bind()를 사용하였다.

예시를 살펴보자.

// UI 로직
// require 생략

const InputView = {
  readPurchaseAmount(callback) {
    Console.readLine(MESSAGE.ASK_PURCHASE_AMOUNT, callback);
  },
  
  // ... //
};
// 핵심 로직
// require 생략

class Game {
  start() {
    InputView.readPurchaseAmount(this.handlePurchaseAmount.bind(this));
  }

  handlePurchaseAmount(amount) {
    PurchaseValidation.checkError(amount);
    this.lottos = LottoMaker.makeLottos(amount);
    
    // ... //
  }
}

핵심 로직 부분에서 bind()가 사용된 것을 볼 수 있다.
왜 이렇게 사용했을까?

this는 메서드가 호출될 때 사용된 객체를 가리킨다.
따라서, this 바인딩을 하지않고 handlePurchaseAmount()만 전달한다면
함수 내부의 thisInputView를 가리키게 될 것이다.
기껏 함수를 핵심 로직 내에서 데이터 주고 받으면서 진행되게 설계해놨는데,
바인딩을 안해서 Game 클래스의 데이터를 활용하지를 못하게 되는 것이다.

의도한대로 작동하려면 thisGame이라는 클래스를 가리켜야한다.

this.handlePurchaseAmount.bind(this)

이 코드는 handlePurchaseAmount 함수에 this를 바인딩했다.
즉, Game 클래스의 프로퍼티에 접근 가능하게 해준 것이다.
이 덕분에 다른 파일에 존재하는 메서드에 인자로 넘어간 handlePurchaseAmount 함수가
의도한대로 Game 클래스 내의 데이터에 접근하여 작동하게 되는 것이다.

총 정리

function callAndapply() {
  console.log(Array.prototype.join.call(arguments));  // "Rki0,백수,26"
  console.log(Array.prototype.join.apply(arguments));  // "Rki0,백수,26"
}

function bindTest() {
  console.log(Array.prototype.join.bind(arguments));  // [Function: bound join]
  console.log(Array.prototype.join.bind(arguments)());  // "Rki0,백수,26"
}

callAndapply("Rki0", "백수", 26);
bindTest("Rki0", "백수", 26);

call(), apply()는 바인딩을 하고, 호출한다.
bind()는 바인딩을 하고, 선언까지만한다.

참고 자료

call() - MDN docs
apply() - MDN docs
bind() - MDN docs
inpa님 블로그
wooooooak님 블로그

profile
나를 믿는 사람들을, 실망시키지 않도록

0개의 댓글