바인딩. 막히는게 있어서 질문을 올리면 꽤나 자주 등장하는 단어이다.
이 "바인딩"에 관련 메서드들이 있다. 바로call
,apply
,bind
이다.
모두this
의 바인딩을 변경한다는 공통점이 있는데, 어떤게 다른지 살펴보자.
우선 MDN 문서부터 살펴보자.
call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.
- MDN docs -
사용법은 다음과 같다.
func.call(thisArg[, arg1[, arg2[, ...]]])
음...와닿지않는다. 예시를 살펴보자.
const obj = {
name: "Rki0",
job: "백수",
};
function callTest() {
return this;
}
console.log(callTest()); // window 객체
callTest
함수가 반환하는 this
는 window
객체를 가리킨다.
그런데...나는 죽어도 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
파라미터에 들어가서 출력된다.
그치만 this
는 window
를 가리키고 있기 때문에 this.job
은 undefined
가 나온다.
그래서 obj
객체를 this
로 가리키기 위해서 call()
을 사용해 바인딩을 했다.
그 결과, this.job
은 obj
객체의 job
을 가져올 수 있었지만,
callTest
함수 자체의 파라미터인 job
에는 아무것도 들어오지 않은 undefined
가 출력된다.
그래서 call()
의 두 번째 인자에 인자를 넣어줬다.
이 인자는 callTest
함수의 파라미터에 해당하는 값이다.
그 결과, job
과 this.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
함수에서 this
는 window
객체를 가리킨다.
Food
함수에서도 마찬가지 this
는 window
..라고 생각했으나
new
생성자를 통해 호출되었으므로, 여기서는 this
가 Food
에 바인딩된다.
이제 call()
에 대한 부분을 살펴보자.
Food
함수 내에서 call()
을 사용해 Product
함수에 this
를 바인딩해줬다.
이 this
는 방금 설명했듯이 Food
에 바인딩된 this
이다.
따라서, Product
에서 this.name === Food.name
이고, this.price === Food.price
가 된다.
생성자로 Food
함수를 생성할 때, 인자로 name
과 prize
를 전달했다.
이 인자는 Product
함수까지 전달되며,
Product
함수는 call()
에 의해 Food
의 this
에 바인딩이 되었고,
Food
함수는 본인은 name
과 prize
를 별도로 어딘가에 저장하지 않았음에도
Product
함수에서 그 값들을 꺼내 쓸 수 있게되었다.
정리해보자.
이 모든 것은 Product
함수가 Food
함수의 this
를 바인딩하고 있기 때문이다.
Food
함수와 Product
함수가 같은 this
를 사용하고 있으며,
그 this
는 Food
함수의 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
를 정의하면 다른 객체의 파라미터나 메소드를 사용할 수 있다.
우선 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 객체
나는 죽어도 obj
를 applyTest()
를 통해서 접근하고싶다.
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
가 전역 객체를 대체한다고한다.
따라서, 위 예제에서 사용된 null
은 window
객체, 즉 전역 객체를 의미하기에
numbers
에 대한 접근이 가능했던 것이다.
const
로 선언되어서 혹시나 이해가 잘 안된다면
var numbers = [5, 6, 2, 3, 7];
var
로 사용했을 때의 경우를 생각하면 된다. 결과는 똑같다.
어찌되었든 전역 객체에 들어있는 프로퍼티라서 접근이 가능하다는 의미이다.
추가로, null
대신 undefined
로 사용해도 같은 결과가 나온다.
const max = Math.max.apply(undefined, numbers);
음...아무튼 둘 다 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
바인딩을 통해 다른 객체의 프로퍼티를 사용할 수 있다는 것이다.
우선 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
함수 호출 시 의도했던 값이 출력된다.
이 부분은 필자가 우테코 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()
만 전달한다면
함수 내부의 this
는 InputView
를 가리키게 될 것이다.
기껏 함수를 핵심 로직 내에서 데이터 주고 받으면서 진행되게 설계해놨는데,
바인딩을 안해서 Game
클래스의 데이터를 활용하지를 못하게 되는 것이다.
의도한대로 작동하려면 this
는 Game
이라는 클래스를 가리켜야한다.
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님 블로그