
개인적으로 제가 많이 익숙하지 않다고 생각하는 주제인 'call, apply, bind' 에 대해서 작성을 진행해보고자 합니다. 이 블로그 포스트를 계기로 자바스크립트에 대한 이해가 한 층 더 깊어지셨으면 좋겠습니다.
'call, apply, bind'에 대해 본격적으로 공부를 들어가기 전에 가장 먼저 이 키워드가 어떻게 생겨났는지, 그리고 왜 생겨났는 지에 대해서 알아야 한다는 생각이 들었습니다.
'call'에 대해 알아보기 전에 가장 먼저 알아야 할 것이 있는데, 그것은 바로 'this' 입니다. 'this'는 간단하게 말하자면 '대상'을 의미합니다. 'this'에 대해 자세하게 알아가고 싶으시다면 이 웹사이트를 참고해주시면 좋을 것 같습니다. 참고자료 2, 3번을 보시면 this에 대해 자세하게 알 수 있습니다. 이 글을 보시면 '현재 'this'가 어떤 대상에 의해 호출되고 불러와 지는가?'가 바로 this라고 볼 수 있습니다.
그런데 어떤 경우에는 현재 호출되고 있는 대상이 아닌 다른 대상을 위해 this를 사용하고 싶은 경우가 생길 수 있습니다.
function Product(name, price) {
this.name = name;
this.price = price;
console.log(this);
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
console.log(new Food('cheese', 5).name);
// expected output: "cheese"
다음 코드는 '참고자료 1'에 들어있는 예제코드입니다. 가장 먼저 Product 내부에 들어있는 'this'에 대해서 생각해보아야 합니다. 저 this는 호출되어지는 대상이 바로 Product이기 때문에 자연스럽게 Product가 this라고 생각할 수 있을 것 같습니다. 그러나 이는 엄밀히 말하자면 정확하지는 않습니다. 그 이유는 바로 함수를 호출하는 대상이 함수 그 자체가 아니기 때문입니다. console.log(this)를 브라우저 내에서 실행해본 결과, windows 객체가 나오게 됩니다.
그렇기 때문에 'new' 생성자를 이용해 인위적으로 'this'를 '비어있는 객체'로 정의한 뒤, 이 '객체'에 name, price라는 프로퍼티를 적용해야 올바른 사용이 가능해집니다. 말이 좀 어렵나요? 그렇다면 한번 예시를 통해 코드를 살펴보기로 하겠습니다.
function User(name) {
this.name = name;
this.isAdmin = false;
console.log(this);
}
const user1 = User("빨강")
const user2 = new User("보라");
예시로 작성한 코드에서는 다음과 같이 'User'라는 이름의 함수가 존재합니다. 그리고 이 함수는 현재 'this'라는 키워드를 사용하고 있습니다. 'this.name'과 'this.isAdmin'이 바로 그 예시이지요. 한번 실험을 진행해 보도록 하겠습니다. user1은 'new' 키워드를 붙이지 않고 실행을 해보고, user2는 'new' 키워드를 붙여본 채로 실행을 해보기로 하겠습니다.
결과는 다음과 같이 나오게 됩니다.

결과는 놀랍게도 'new'를 적용한 결과와 그렇지 않은 결과가 다르게 나타납니다. 그 이유는 바로 무엇일까요? 가장 먼저 user1부터 살펴보기로 합시다. user1의 경우에는 아마 예상했던 결과였을 것입니다. 이전에 설명을 드렸다시피, 현재 User()의 경우에는 전역적인 환경에서 실행을 했기 때문입니다. 그런데, 이는 user2의 경우에도 마찬가지 아닌가요? 결국 'new'가 이런 변화를 만들어낸 것이 아닌가 하는 생각을 하게 됩니다.
자, 이번에는 'new'의 적용으로 user2에 어떤 변화가 일어났는 지 살펴보기로 합시다.
function User(name) {
// this = {} -> 'new'의 실행으로
// 함수 내부에 비어있는 객체가 'this'가 되는 것으로 시작된다.
this.name = name; // this = { name: name }
this.isAdmin = false; // this = { name: name, isAdmin: false }
console.log(this);
// return this ( { name: name, isAdmin: false } )
}
const user2 = new User("보라");
가장 먼저 'new' 키워드의 선언으로 User 함수에 this가 비어있는 객체를 가리킬 수 있도록 설정합니다. 즉, 함수 내부에 비어있는 객체가 'this' 가 되는 것으로 시작됩니다. 그 다음에는 this.name과 this.Admin에 각각 값을 할당해주고 있는데, 결과적으로 비어있는 객체에 값을 할당한다는 것과 같은 이치로 생각해 주시면 됩니다.
마지막에는 this라고 하는 객체를 리턴해줍니다. 결과적으로 해당 객체를 할당받을 수 있게 되는 것이죠. 이것이 바로 'new 키워드'의 결과로 나타나는 현상이라고 볼 수 있을 것 같습니다.
좀 더 자세한 내용은 참고자료 4를 참조해주시면 좋을 것 같습니다.
그런데, 이런 경우를 생각해보기로 합시다. 만약에 현재 함수 A에 존재하고 있는 'this'의 대상('new' 키워드로 만들어진 '객체 A')'을 함수 B에서 작성된 'this'('new' 키워드로 만들어진 '객체 B')로 바꾸려고 한다면, 즉 '재할당'을 진행하고자 한다면 어떻게 해야 할까요? 이를 위하여 나타난 것이 바로 'call'입니다. 이 메서드는 Function 객체의 Prototype에 존재하는 자바스크립트의 내장 메서드이기 때문에 Product 함수 내부에서 직접적으로 불러올 수 있습니다. 자바스크립트에서는 함수도 그 자체로 '객체'라는 사실을 잊어서는 안됩니다.
Product.call(this, name, price);
지금부터 이 코드에 대해서 자세하게 살펴봅시다.
Product 함수로부터 불러온 call()의 첫 번째 인자로 'this'가 들어있는 것을 알 수 있습니다. 그렇다면 이 'this'는 무엇을 의미할까요? 바로 '호출되어지는 대상'을 의미합니다. 생각해보면 저 'Product.call()'을 호출하고 있는 대상은 함수 Food 내부에 저 코드가 들어있기 때문에 함수 Food라고 생각할 수 있습니다. 그러나 이는 사실 정확하지는 않습니다. 'Food()'라는 코드만으로 'this'의 재할당이 이루어지지 않는 이유는 바로 'new' 선언을 해야 하기 때문입니다. 그렇지 않으면 결국 해당 함수를 호출한 대상, 즉 Window(브라우저에 한정합니다.)가 됩니다. 결과적으로 'new' 선언을 통해 Product에서 초기화되어야 할 객체 this를 마치 Food의 this처럼 활용할 수 있습니다.
console.log(new Food('cheese', 5).name);
따라서 다음 코드에 나와있는 대로 'new'를 선언해야 'this'에 비어있는 객체가 생성되고, 이 객체 내부에 Food() 함수의 실행으로 불러온 this를 적용할 수 있게 되고 최종적으로 this가 리턴되기 때문에 우리 눈에서는 '객체'로 보이게 되는 것입니다.
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
이제 다시 이 코드로 돌아와서, 2번째와 3번째 인자인 'name'과 'price'에 대해서 살펴봅시다. Food 함수를 통해 매개변수 name과 price에 할당된 값을 call 함수에 넣어주게 되면 결과적으로 Product함수 내부 로직을 이용해서 처리를 진행하게 되고, 그 결과로 리턴된 this 객체를 Food에서 사용할 수 있게 됩니다. 물론 어디까지나 이는 'new' 연산자를 사용했다는 전제가 들어 있습니다.
여기에 더해 category 프로퍼티까지 할당하게 되면, 결과적으로 Food의 new 연산자로 만들어진 'this' 객체는 name, price, category 프로퍼티와 이에 대한 값을 모두 가질 수 있게 됩니다.
이번에는 call과 비슷하지만 다른 apply에 대해서 살펴보기로 합시다.
let obj = { name: 'Jack' };
let say = function (city) {
return console.log(
`Hello ${this.name}, Well-come to ${city}`
)
};
say.apply(obj, ['seoul']);
// VM103:2 Hello Jack, Well-come to seoul
// call과 비교
// function Product(name, price) {
// this.name = name;
// this.price = price;
// console.log(this);
// }
// function Food(name, price) {
// Product.call(this, name, price);
// this.category = 'food';
// }
먼저 다음 코드를 통해 다른 점을 파악해 보기로 합시다. call의 경우 2번째 이후의 인자의 경우에는 call을 호출한 함수의 1번째 인자 이후와 동일한 순서로 적용됩니다. apply는 단지 이를 두 번째 인자에 '배열' 형태로 적용을 했다는 차이점이 존재합니다. 결과적으로 취사 선택을 통해 좀 더 유용하게 쓰일 수 있는 함수를 사용하면 될 것 같습니다.
var obj = {
name: 'shpark'
};
function bindTest() {
console.log(this.name);
}
bindTest(); // undefined 출력
var bindTest2 = bindTest.bind(obj); // bindTest 함수에 obj 객체를 bind
bindTest2(); // obj의 name 값인 shpark 이 표출
이 함수가 call/apply와 비교되는 가장 큰 특징은 바로 '리턴 값'을 '함수 형태'로 내보낸다는 것입니다. bind()는 마치 해당 메서드 내부에 들어있는 인수(객체)를 감쌀 수 있다는 특징을 가지고 있습니다. 예를 들어, bindTest()가 가리키고 있는 'this'는 bind() 내부에 들어있는 첫 번째 인자, 즉 'obj'가 됩니다. 그리고 리턴된 함수를 호출하게 되면 bind()를 호출한 함수의 'this'가 obj로 설정된 채로 실행이 되게 됩니다.
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX();
// 9 반환 - 함수가 전역 스코프에서 호출됐음
// module과 바인딩된 'this'가 있는 새로운 함수 생성
// 신입 프로그래머는 전역 변수 x와
// module의 속성 x를 혼동할 수 있음
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
다음 코드는 mdn에서 나온 코드인데, 이를 간단하게 살펴보기로 합시다. retrieveX 함수는 module.getX라는 이름의 함수를 복제합니다. 그 다음에 retrieveX 함수를 실행시키면 결과적으로 9가 출력됩니다. 그 이유는 바로 해당 함수를 호출한 영역이 바로 전역 스코프이기에, this.x = 9가 적용되기 때문입니다. 그런데, 이를 방지하고자 종속적으로 retrieveX.bind(module)를 활용하게 되면 첫 번째 인자의 module 객체가 this가 되기 때문에 boundGetX()에서의 this는 module 객체의 this인 81을 가리키게 됩니다.
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 1초 지체 후 bloom 선언
LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' +
this.petalCount + ' petals!');
};
var flower = new LateBloomer();
flower.bloom();
// 1초 뒤, 'declare' 메소드 유발
다음 코드는 bind() 함수를 상당히 유용하게 사용할 수 있는 setTimeout()에 대한 예시입니다. setTimeout은 기본적으로 전역 메서드이기 때문에 호출되는 대상도 전역적입니다. 따라서 bind()를 사용해서 이 내부에 들어있는 'this'를 해당 코드가 실행된 함수의 this로 만들어주어야 합니다.
// 에러 발생 코드!
class EventObject {
element = null;
constructor() {
this.element = document.querySelector('.myName');
}
getName() {
return 'My name is EventObject'
}
addEvent() {
this.element.addEventListener('click', this.handlerClick);
}
handlerClick() {
alert(this.getName()); // this는 타겟 요소 이기 때문에 에러발생
}
}
// 개선안
class EventObject {
(...)
addEvent() {
this.element.addEventListener(
'click', this.handlerClick.bind(this));
}
handlerClick() {
alert(this.getName()); // this가 바인딩 되어서 정상적으로 작동한다.
}
}
두 번째로 bind()를 유용하게 사용할 수 있는 예시는 바로 addEventListener()입니다. 에러 발생 코드 란의 코드에서 에러가 발생하는 이유는 바로 addEventListener() 내부에서 사용한 this가 바로 이벤트 리스너를 걸어준 this.element이지, EventObject 객체가 아니기 때문입니다. 이를 방지하기 위해서는 bind() 함수를 해당 메서드에 걸어준 뒤, 'this'를 명시해서 객체가 EventObject임을 명시해 주어야 합니다.
이번에는 bind() 함수를 다양하게 사용해보는 연습을 진행해 보기로 하겠습니다.
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// 선행될 인수를 설정하여 함수를 생성합니다.
var leadingThirtysevenList = list.bind(null, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
function addArguments(arg1, arg2) {
return arg1 + arg2
}
var result1 = addArguments(1, 2); // 3
// 첫 번째 인수를 지정하여 함수를 생성합니다.
var addThirtySeven = addArguments.bind(null, 37);
var result2 = addThirtySeven(5); // 37 + 5 = 42
// 두 번째 인수는 무시됩니다.
var result3 = addThirtySeven(5, 10); // 37 + 5 = 42
다음 코드를 통해 bind()에 대해 좀 더 자세하게 살펴보기로 하겠습니다. 가장 먼저 list 함수 내부에 arguments를 넣은 모습을 볼 수 있는데, 이를 적용하게 되면 따로 list() 함수 내부에 인자를 넣을 필요 없이 해당 인수들의 Array 객체를 전달하게 됩니다. 따라서 다음처럼 [1, 2, 3]을 적용할 수 있게 됩니다.
가장 먼저 list의 bind()의 첫 번째 인자가 null인 것을 볼 수 있는데, 두 번째 이후의 인자부터는 마치 해당 함수의 인자로 처리가 되기 때문에 list(37)과 동일하게 사용을 하는 것이 가능해집니다. bind()는 리턴값으로 해당 함수를 리턴하기 때문에, 이 내부에서 다시 1, 2, 3을 넣으면 list.bind(37)(1,2,3) 의 함수가 만들어집니다. (bind()는 함수를 리턴한다는 사실을 기억하세요!)
지금까지 call/apply/bind에 대한 설명이었습니다! 읽어주셔서 감사합니다 🙂
참고자료:
1.mdn - Function.prototype.call(): https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call
2. 자바스크립트의 this()는 무엇인가: https://www.zerocho.com/category/JavaScript/post/5b0645cc7e3e36001bf676eb
3. mdn - this:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this
4. new 연산자와 생성자 함수:
https://ko.javascript.info/constructor-new
5. apply 함수의 사용:
https://basemenks.tistory.com/15
6. javascript의 bind 함수 이해하기:
https://shplab.tistory.com/entry/Javascript-bind-%ED%95%A8%EC%88%98-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
7. mdn - Function.prototype.bind():
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
8. 이벤트 리스너와 bind:
https://velog.io/@suld2495/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88%EC%99%80-bind
9. arguments:
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/arguments
역시 상준님은 멋있군요