또한, 상속과 super 등을 사용하지 않고 함수를 독립적으로 뺴서 객체에게 사용할때 call을 사용한다.
function showThisName() {
// 여기서 this는 window를 가리킨다.
// window.name은 "" 빈문자열
console.log(this.name);
}
showThisName(); // "" 혹은 undefined
const mike = { name: "Mike" };
showThisName.call(mike); // Mike
참고로, 자바스크립트 환경에서는 window가 전역변수이고, Node.js 환경에서 global이 전역변수가 된다.
이제 아래의 코드를 보자!
let kim = { name: "kim", first: 100, second: 20 };
let lee = { name: "kim", first: 10, second: 20 };
function sum() {
return this.first + this.second;
}
// sum();
sum.call();
// 둘은 같다. 그런데, 왜 구지 .call()을 사용해야할까?
// 모든 함수는 .call()이라는 메소드가 내장되있다.
sum.call(kim);
// sum() 안의 this 값은 kim의 this를 가리킨다.
console.log(sum.call(kim)); // 출력값 : 120
// sum()은 kim 이라는 객체의 member가 아니었는데,
// call()이라는 특이한 메소드를 통해서, sum()이라는 함수가 kim의 member 메소드가 된 것이다.
console.log(sum.call(lee)); // 출력값 : 30
// .call() 이라고 하는 것은 인자를 몇 개 더 받을 수 있는데
function sum2(prefix, another = "") {
return prefix + another + (this.first + this.second);
}
// prefix의 값이 없기 때문에, NaN
console.log(sum2.call(kim));
console.log(sum2.call(lee));
// [필수인자] call()이라는 함수의 첫번째 인자로는
// call()을 붙여줄 함수의 내부적으로 this를 뭘로 해줄 것인가가 오고,
// [optional] 2번째 인자부터는 우리가 호출하려고 하는 함수의 파라미터로 들어갈 인자값들이 들어간다.
console.log(sum2.call(kim, "=> ")); // 출력값 : => 120
console.log(sum2.call(lee, "두번째 인자 ", "세번째 인자 "));
// 출력값 : 두번째 인자 세번째 인자 30
// call()
// 상속과 super 등을 사용하지 않고 함수를 독립적으로 뺴서 객체에게 사용할때 call을 사용한다.
// call의 유사품으로는 apply가 있는데, 사용법은 거의 비슷하다.
apply에 대해서 들어가기에 앞서, 사전 지식 몇가지 정리해보자.
함수도 객체이다.
함수에는 .call과 마찬가지로 .apply라는 내장 메소드를 가지고 있다.
spread operator, rest parameter가 탄생하기 이전엔 apply가 많이 쓰였다.
아래의 코드는 똑같은 sum()함수의 사용법이다.
function sum(arg1, arg2) {
return arg1 + arg2;
}
console.log(sum.apply(null, [1, 2])); // 3
console.log(sum(1, 2)); // 3
그런데, 구지 첫번째 인자로 null을 사용하면서까지 apply를 사용해야할 이유가 있을까??
function sum() {
let _sum = 0;
for (const name in this) {
// for문이 this를 열거하는 과정에서 sum이라는 함수도 더하기를 시도하기 때문에
// 에러 방지를 위해서 if 절이 필요하다.
if( typeof this[name] !== 'function' )
_sum += this[name];
}
return _sum;
}
let o1 = { val1: 1, val2: 2, val3: 3, sum:sum }
위의 코드를 보면, o1
객체 안에 sum
이라는 메소드 있다는 것을 알 수있다. 즉, sum을 사용하기 위해서 객체 안에 장착해놓을 것인데, sum을 사용하기 위해서 객체마다 sum을 장착해야 한다면, 상당히 번거로울 것이다.
그래서, 출현한 것이 바로 apply()
이다.
// apply를 사용하는 구체적인 이유
let o1 = { val1: 1, val2: 2, val3: 3 };
let o2 = { v1: 10, v2: 50, v3: 100, v4: 25 };
function sum() {
let _sum = 0;
// for in 문으로 this라고 하는 객체의 값들을 하나씩 꺼네서, 더한다.
// 이 맥락에서 this는 현재 정해져있지 않다.
// this는 호출 할 때, 정해진다.
for (const name in this) {
_sum += this[name];
}
return _sum;
}
// 첫번째 인자로 null 대신에 객체를 짚어넣었다.
// sum()에서 this = o1 이다.
console.log(sum.apply(o1)); // 6
console.log(sum.apply(o2)); // 185
let obj = { num:2 };
let obj2 = { num:5 };
const addToThis = function (a,b,c) {
return this.num + a + b + c;
};
let arr = [1,2,3, 4];
console.log( addToThis.apply(obj, arr) ); // 8
// => addToThis() 는 a,b,c라는 3개의 인자만의 받는다.
// 그래서, arr 배열 안에서 첫 3개의 요소만이 obj의 num값과 더해진 것이다.
console.log( addToThis.apply(obj2, arr) ); // 11
다음은 apply를 이용해 배열 인자를 풀어서 넘기는 예제들이다.
// null을 this로 지정합니다. Math는 생성자가 아니므로 this를 지정할 필요가 없다.
Math.max.apply(null, [5,4,1,6,2]) // 6
// spread operator의 도입으로 굳이 apply를 이용할 필요가 없어졌다.
Math.max(...[5,4,1,6,2]) // 6
다음은 prototype을 빌려 실행하는 예제를 보여주고 있습니다.
// '피,땀,눈물'을 this로 지정한다.
''.split.call('피,땀,눈물', ',')
// 다음과 정확히 동일한 결과를 리턴한다.
'피,땀,눈물'.split(',')
let allDivs = document.querySelectorAll('div'); // NodeList라는 유사 배열이다.
// allDivs를 this로 지정한다.
[].map.call(allDivs, function(el) {
return el.className
})
// allDivs는 유사 배열이므로 map 메소드가 존재하지 않는다.
// 그러나, Array prototype으로부터 map 메소드를 빌려와 this를 넘겨 map을 실행할 수 있다.
이제 apply를 이용해서, 매개 변수를 받지 않는 독립된 함수를 객체가 활용할 수있게 되었다.
call()과 사용법과 용도도 비슷하다. 그러나, call()과의 차이점은 2번째 인자로 배열을 받을 수 있어서 객체가 아니라더라도 그리고 함수가 매개변수를 받지 않더라도 해당 함수에 배열 형식의 인자를 짚어넣어줄 수있게 됐다는 점이다.
즉, 함수의 재사용성을 극대화 시키기 위한 방법으로 apply()가 출현했다고 볼 수있다.
여기까지 call과 apply에 대해서 정리하자면, 다음과 같다.
this는 누군가에의해 호출되기 전까진 window 를 가리킨다
그 누군가를 명확히 하기위해 call과 apply라는 메소드를 사용한다
차이점은 apply는 연산에 필요한 인자값을 배열로 받는반면
call은 단일 인수로 받는다
bob.call(bill, 2,’goodboy’)
Bob.apply(bill,[1,”-/:;”])
그러면 bind는 뭘까??
call과 상당히 헷갈리기 때문에, 둘의 차이에 관해서 정리해보자!
call은 실행되는 함수의 this값을 원하는 객체로 바꿔서 실행할 수 있게 해준다.
bind는 실행되는 함수의 this값을 원하는 객체로 고정시키는 새로운 함수를 만들어낸다.
call : 외부의 function을 내부로 끌어다 쓸수있게 해준다.
bind: 새로운 함수를 clone 하여 this의 주체를 ()안의 객체로 바꾼 새로운 function를 생성, 복제한 부모 함수에게는 영향을 주지 않는다.
let kim = { name: "kim", first: 100, second: 20 };
let lee = { name: "kim", first: 10, second: 20 };
function sum(prefix, another = "") {
return prefix + another + (this.first + this.second);
}
let kimSum = sum.bind( kim, '-> ' );
console.log( kimSum() );
call과 bind는 닮은 듯이 다른데,
call은 실행할 때, 함수의 this 값을 바꾸고, bind는 어떤 함수의 내부적으로 this값이 영구적으로 변경된 새로운 함수를 return 한다.
위에 설명만으로는 부족하니, 아래의 코드를 보면서 이해해보자.
필자의 경우, 아래의 코드를 통해 call과 bind의 차이를 확실하게 이해할 수 있었다.
let obj = { num: 2 };
const addToThis = function (a, b, c) {
return this.num + a + b + c;
};
console.log(addToThis.bind(obj, 1, 2, 3)); // function
// bind는 함수를 return한다. => 이 함수는 this값이 obj의 num으로 지정되있다.
// bind로 call과 같은 출력값을 내려면,
console.log(addToThis.bind(obj, 1, 2, 3)()); // 8
console.log(addToThis.bind(obj)(1, 2, 3)); // 8
console.log(addToThis.call(obj, 1, 2, 3)); // 8
// call은 출력값(결과값)을 return한다.
// bind는 원본에 영향을 주지 않는 독립된 새로운 함수를 return하기 때문에,
// 변수 담아뒀다가 필요할 때마다 재사용하는 방식을 많이 쓴다.
let bound = addToThis.bind(obj);
console.log(bound(1, 2, 3)); // 8
나중에 리엑트에서는 .bind(this)
형태로 많이 쓰이는데, 나중에 리엑트 정리하면서 더 자세히 알아보도록 하자!!
bind는 call, apply에 비해 비교적 유용한 사용 예가 많이 존재한다. 두 가지 예를 먼저 살펴보자.
bind는 이벤트 핸들러에서 이벤트 객체 대신 다른 값을 전달하고자 할 때 유용하다. 아래와 같은 상황을 가정해보자.
동적으로 여러 개의 버튼을 만들고, 각각의 이벤트 핸들러에 각기 다른 값을 바인딩해야 할 경우를 생각해보자.
다음 예제에서 기대하는 결과는 각 버튼을 클릭할 때에, alert에 사용자 정보가 표시되게 만드는 것이다.
https://codesandbox.io/s/bind-method-hagseub-nlmzi?from-embed
위와 같이 코드를 작성하면, 동적으로 생성되는 각각의 버튼을 클릭하면 user에 원하는 값이 제대로 전달되지 않을 것이다. (user에 실제로 무엇이 찍히는지 console.log로 확인해보자)
이 때 bind를 이용해 인자를 지정하되, 즉시 실행되지 않게 만들 수 있다. 이 때에 this값은 중요하지 않으므로 null과 같이 아무것도 넘기지 않아도 된다.
users.forEach(user => {
let btn = document.createElement('button');
btn.textContent = user.name;
btn.onclick = handleClick.bind(null, user);
target.appendChild(btn);
});
bind를 사용하지 않고 익명 함수로 문제를 해결할 수도 있다.
users.forEach(user => {
let btn = document.createElement('button')
btn.textContent = user.name
btn.onclick = () => {
handleClick(user)
}
target.appendChild(btn)
});
setTimeout은 시간 지연을 일으킨 후 함수를 비동기적으로 실행하게 하는 함수이다. 이 함수는 명시적으로 항상 window 객체를 this 바인딩하는 특징이 있다. 그래서 다음과 같은 문제 상황이 발생할 수 있다.
class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
printArea() {
console.log('사각형의 넓이는 ' + this.getArea() + ' 입니다')
}
printSync() {
// 즉시 사각형의 넓이를 콘솔에 표시한다
this.printArea()
}
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시한다
setTimeout(this.printArea, 2000)
}
}
let box = new Rectangle(40, 20)
box.printSync() // '사각형의 넓이는 800 입니다'
box.printAsync() // 에러 발생!
에러를 통해 this가 Rectangle의 인스턴스가 아니라는 것을 확인할 수 있다.
Uncaught TypeError: this.getArea is not a function
at printArea (<anonymous>:12:36)
console.log(this)
를 추가해 직접 확인해보자.이 문제를 해결하기 위해 bind를 이용할 수 있습니다. printAsync 부분을 다음과 같이 바꿔보다.
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시한다
setTimeout(this.printArea.bind(this), 2000)
}
화살표 함수를 도입해보자. 다음은 잘 작동하는 예제이다. 화살표 함수의 this는 뭔가 다르게 작동되고 있다는 사실을 인지했나?
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시한다
setTimeout(() => {
this.printArea()
}, 2000)
}
=>call
은 첫번째인자로this
값을 지정할 객체를 받고, 두번째 인자부터는 추가적으로 넣어줄 인자들을 짚어넣을 수있다.
=>call
은 함수가 곧바로 실행되고, 그에 따른 결과값을 return 한다.
=>apply
는 모든 면에서call
과 같지만, 추가적으로 넣어줄 인자는 배열형태도 넣어줘야한다.
=>apply
은 함수가 곧바로 실행되고, 그에 따른 결과값을 return 한다.
=>bind
는 첫번째인자로 this값을 지정할 객체를 받는다. 두번째인자로call
처럼 넣어줄 수도있다.
=> 그러나,bind
는 함수의 return 값이 아닌 첫번째 인자의 객체값을this
로 지정한 독립된 새로운 함수를 return한다.
=> 이 함수는 보통 재사용성을 위해서 변수에 할당되어진 뒤에 필요에 따라서 사용된다.