this Review
this는 함수 실행시 호출(invocation) 방법에 의해 결정되는 특별한 객체입니다. 함수 실행시 결정되므로, 실행되는 맥락(execution context)에 따라 this가 다르게 결정됩니다.
this는 다섯가지 바인딩 패턴이 존재합니다 (non-strict mode를 기준으로 설명합니다)
패턴바인딩되는 객체설명
Global | window | 콘솔에서 this를 조회할 때 출력되는 값 (흔히 사용되지 않음) |
Function 호출 | window | Function 호출시 this를 사용할 이유는 없음 (흔히 사용되지 않음) |
Method 호출 | 부모 객체 (실행 시점에 온점 왼쪽에 있는 객체) | 하나의 객체에 값과 연관된 메소드를 묶어서 사용할 때 주로 사용함 |
new 키워드를 이용한 생성자 호출 | 새롭게 생성된 인스턴스 객체 | 객체 지향 프로그래밍에서 주로 사용함 |
.call 또는 .apply 호출 | 첫번째 인자로 전달된 객체 | this 값을 특정할 때 사용하며, 특히 apply의 경우 배열의 엘리먼트를 풀어서 인자로 넘기고자 할 때 유용함 |
메소드 호출은 객체.메소드() 과 같이 객체 내에 메소드를 호출하는 방법을 의미합니다.
단순 객체를 사용한 Singleton 패턴에서 이러한 예제를 흔히 볼 수 있습니다. 다음은 카운터를 구현한 예제입니다.
let counter1 = {
value: 0,
increase: function() {
this.value++; // 메소드 호출을 할 경우, this는 counter1을 가리킵니다
},
decrease: function() {
this.value--;
},
getValue: function() {
return this.value;
}
}
counter1.increase();
counter1.increase();
counter1.increase();
counter1.decrease();
counter1.getValue(); // 2
Singleton 패턴은 단 하나의 객체만 만들 수 있으므로, 똑같은 기능을 하는 카운터를 여러 개 만드려면, 아래 예제 코드와 같이 클로저 모듈 패턴을 이용하거나, 클래스로 만들어서 생성자 호출과 같이 사용할 수 있습니다.
function makeCounter() {
return {
value: 0,
increase: function() {
this.value++; // 메소드 호출을 할 경우, this는 makeCounter 함수가 리턴하는 익명의 객체입니다
},
decrease: function() {
this.value--;
},
getValue: function() {
return this.value;
}
}
}
let counter1 = makeCounter();
counter1.increase();
counter1.getValue(); // 1
let counter2 = makeCounter();
counter2.decrease();
counter2.decrease();
counter2.getValue(); // -2
생성자 호출은 객체.메소드() 과 같이 객체 내에 메소드를 호출하는 방법과 비슷하지만, 객체가 new 키워드를 이용해서 만들어졌다는 것이 다릅니다. 이 때의 객체는 우리가 인스턴스라고 부릅니다. 즉 인스턴스.메소드() 의 형태의 호출입니다. 카운터를 클래스로 만들어봅시다.
class Counter {
constructor() {
this.value = 0; // 생성자 호출을 할 경우, this는 new 키워드로 생성한 Counter의 인스턴스입니다
}
increase() {
this.value++;
}
decrease() {
this.value--;
}
getValue() {
return this.value;
}
}
let counter1 = new Counter(); // 생성자 호출
counter1.increase();
counter1.getValue(); // 1
call, apply 호출은 명시적으로 this를 지정하고 싶을 때 사용합니다. 첫번째 인자가 항상 this값이 됩니다.
주의: 지금은 코드 하나하나를 다 이해할 필요 없이 "아 이렇게도 쓸 수 있구나", "생각보다 많이 쓰이는구나" 정도만 이해하고 넘어가도 충분합니다. call의 유용한 예제는 추후에 다룹니다. 다만, 첫번째 인자가 this라는 점만 반드시 기억하고 넘어가세요.
다음은 apply를 이용해 배열 인자를 풀어서 넘기는 예제입니다.
예제// null을 this로 지정합니다. Math는 생성자가 아니므로 this를 지정할 필요가 없습니다.
// 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을 실행할 수 있습니다.
다음은 객체 지향 프로그래밍에서의 상속을 구현하기 위한 call, apply 사용입니다.
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
// 인자가 많으면 Product.apply(this, arguments) 가 더 유용합니다.
this.category = 'food';
}
let cheese = new Food('feta', 5000); // cheess는 Food이면서 Product입니다.
.bind Review
.bind는 .call과 유사하게 this 및 인자를 바인딩하나, 당장 실행하는 것이 아닌 바인딩된 함수를 리턴하는 함수입니다.
첫번째 인자는 this, 두번째 인자부터는 필요한 파라미터를 전달합니다.
fn.bind(this값, 인자1, 인자2, ...)
bind는 이벤트 핸들러에서 이벤트 객체 대신 다른 값을 전달하고자 할 때 유용합니다. 아래와 같은 상황을 가정해봅시다.
<button id="btn">클릭하세요</button>
let btn = document.querySelector('#btn');
btn.onclick = handleClick;
function handleClick() {
console.log(this);
}
위의 예제에서, handleClick에서 확인하는 this 값은 무엇일까요?
위의 예제에서 bind를 써서 this를 변경하는 방법을 알아봅시다.
let btn = document.querySelector('#btn');
// 추후 이벤트에 의해 불리게 될 함수에서, this는 {hello: 'world'}가 됩니다.
btn.onclick = handleClick.bind({ hello: 'world'});
function handleClick() {
console.log(this);
}
조금 더 유용한 예제를 살펴봅시다. 동적으로 여러 개의 버튼을 만들고, 각각의 이벤트 핸들러에 각기 다른 값을 바인딩해야 할 경우를 생각해봅시다. (Twittler 스프린트에서 이와 비슷한 구현을 경험해보았을 것입니다.)
다음 예제에서 기대하는 결과는 각 버튼을 클릭할 때에, 이름이 콘솔로 표시되게 만드는 것입니다.
좀 더 유용한 예제
<div id="target"></div>
let target = document.querySelector('#target');
let users = ['김코딩', '박해커', '최초보'];
users.forEach(function(user) {
let btn = document.createElement('button');
btn.textContent = user;
btn.onclick = handleClick;
target.appendChild(btn);
});
function handleClick() {
console.log(this);
}
위와 같이 코드를 작성하면, 동적으로 생성되는 각각의 버튼을 클릭하면 button 엘리먼트 자체가 콘솔에 표시될 것입니다.
이 때 bind를 이용해 출력하고 싶은 값을 this로 넘기거나, 혹은 인자로 보낼 수 있습니다.
Solution 1:
let target = document.querySelector('#target');
let users = ['김코딩', '박해커', '최초보'];
users.forEach(function(user) {
let btn = document.createElement('button');
btn.textContent = user;
btn.onclick = handleClick.bind(user); // 이렇게 바꿔볼 수 있습니다.
target.appendChild(btn);
});
function handleClick() {
console.log(this);
}
Solution 2:
let target = document.querySelector('#target');
let users = ['김코딩', '박해커', '최초보'];
users.forEach(function(user) {
let btn = document.createElement('button');
btn.textContent = user;
btn.onclick = handleClick.bind(null, user); // 굳이 this를 이용하지 않더라도 인자로 넘길 수도 있습니다.
target.appendChild(btn);
});
function handleClick(user) {
console.log(user);
}
bind를 사용하지 않고 익명 함수로 문제를 해결할 수도 있습니다.
Solution 3:
let target = document.querySelector('#target');
let users = ['김코딩', '박해커', '최초보'];
users.forEach(function(user) {
let btn = document.createElement('button');
btn.textContent = user;
btn.onclick = function() {
handleClick(user);
}
target.appendChild(btn);
});
function handleClick(user) {
console.log(user);
}
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)
그렇다면, this 값은 무엇일까요? printArea 함수의 도입 부분에 console.log(this) 를 추가해 직접 확인해보세요.
이 문제를 해결하기 위해 bind를 이용할 수 있습니다. printAsync 부분을 다음과 같이 바꿔봅시다.
Solution 1:
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
setTimeout(this.printArea.bind(this), 2000);
}
익명 함수를 써야 한다면 다음과 같이 바꿀 수 있겠지만, 그리 깔끔한 방법은 아닙니다.
Solution 2 (권장되지 않음):
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
let self = this;
setTimeout(function() {
self.printArea();
}, 2000);
}
화살표 함수를 도입해봅시다. 다음은 잘 작동하는 예제입니다. 화살표 함수의 this는 뭔가 다르게 작동되고 있다는 사실을 알아내셨나요?
Solution 3:
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
setTimeout(() => {
this.printArea();
}, 2000);
}
화살표 함수일 때의 this와, 일반 함수에서의 this 값은 각각 무엇인가요? 어떻게 다른가요?
'Codestates > Full IM' 카테고리의 다른 글
Data Structure Intro (0) | 2020.07.23 |
---|---|
Immersive Prep [TIL] Fibonacci numbers (0) | 2020.07.22 |
Immersive Prep - ES6 Practice (0) | 2020.07.22 |
Immersive Prep - Linting & Testing (0) | 2020.07.21 |
Immersive Prep - Git workflow (0) | 2020.07.20 |