공부하는 방법에 대한 글을 코드스테이츠 깃허브에서 보게 되었다.
내게 부족한것이 무엇인지 생각하면서 공부하는 방법이었다.
글을 보고 자극받아 내 블로그에도 내가 알아둬야할 것들이 무엇인지
추가해 보기로 했다.
함수를 호출하는 방법 중 하나로 함수 호출 시 5가지 바인딩 패턴이 존재한다.
아래 표는 바인딩 패턴에 대한 설명이다.
패턴 | 바인딩되는 객체 | 설명 |
---|---|---|
Mathod 호출 | 부모 객체(실행 시점에 온점 왼쪽에 있는 객체) | 하나의 객체에 값과 연관된 메소드를 묶어서 사용할 때 주로 사용함 |
new 키워드를 이용한 생성자 호출 | 새롭게 생성된 인스턴스 객체 | 객체 지향 프로그래밍에서 주로 사용함 |
.call 또는 .apply 호출 | 첫번째 인자로 전달된 객체 | this값을 특정할 때 사용하며, 특히 apply의 경우 배열의 엘리먼트를 풀어서 인자로 넘기고자 할 때 유용함 |
위 세가지 바인딩 패턴은 꼭 알아둬야한다.
패턴 | 바인딩되는 객체(브라우저) | 바인딩되는 객체(node.js) |
---|---|---|
Global | window (strict mode 에서는 undefined) | module.exports |
Function 호출 | window (strict mode 에서는 undefined) | global |
위 두가지 패턴은 this를 사용하지 않는 것을 권장한다.
함수 호출 시 this를 사용할 이유는 없기 때문이다.
경우에따라 바인딩이 달라지지만 굳이 외울필요는 없고
사용하지 않으면 된다.
메소드 호출은 객체.메소드()
와 같이 객체 내에 메소드를 호출하는 방법을 말한다.
아래 예제를 살펴보면서 어떻게 작동하는지 설명해보자
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
위 표에 설명한것과 같이 메소드 호출은 바인딩되는 객체가 부모로 지정된다 때문에 this.value
는 counter1.value
와 같다.
똑같은 기능을 하는 카운터를 여러개 만든다면
클로저 모듈 패턴을 이용할 수 있다.
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
이런식으로 사용할 경우 this
는 makeCounter()
가 된다.
생성자 호출은 객체.메소드()
와 같이객체 내에 메소드를 호출하는 방법과 비슷하지만,
객체가 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
카운터를 클래스로 만들어보자
클래스를 만드는 방법은 여기서 확인
이 때 this
는 new Counter()
로 생성한 인스턴스가 된다.
클래스를 만들 때 this.value = 0
이라고 사용할 데이터를 정의하고
new Counter()
로 객체를 호출한다.
.call
, .apply
호출은 명시적으로 this를 지정하고싶을 때 사용한다.
첫번째 인자값이 항상 this값이 된다.
call의 유용한 예제는 많지만 지금은 첫번째 인자가 this라는 것만 기억하자
다음은 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을 실행할 수 있습니다.
객체 지향 프로그래밍에서 찾아볼 수 있는 예제
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입니다.
.call
과 유사하게 this 및 인자를 바인딩하나, 당장 실행하는 것이 아닌 비인딩된 함수를 리턴하는 함수이다.
첫번째 인자는 this, 두번째 인자부터 필요한 파라미터를 전달한다.
fn.bind(this값, 인자1, 인자2, ...)
case 1: 이벤트 핸들러
bind는 이벤트 핸들러에서 이벤트 객체 대신 다른 값을 전달하고자 할 때 유용하다.
아래와 같은 상황을 가정해 보자
<button id="btn">클릭하세요</button>
let btn = document.querySelector('#btn')
btn.onclick = handleClick
function handleClick() {
console.log(this)
}
이 때 handlerClick
에서 확인하는 this값은 html 노드 전체이다.
위의 예제에서 bind를 써서 this를 변경하는 방법이다.
let btn = document.querySelector('#btn')
// 추후 이벤트에 의해 불리게 될 함수에서, this는 {hello: 'world'}가 됩니다.
btn.onclick = handleClick.bind({ hello: 'world'})
function handleClick() {
console.log(this)
}
이렇게 하면 버튼을 눌렀을때 HTML 노드 전체가 아닌
{hello: 'world'}
라는 객체를 this값으로 한다.
case 2: setTiemout
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 값은 Rectangle로 확인할 수 있다.
(console.log(this)
를 printArea안에 입력)
다만 setTimeout의 this의 경우 window로 출력된다.
위에서 설명한 것과 같이setTimeout은 명시적으로 항상
window객체를 바인딩하기 때문이다.
이 문제를 해결하기 위해 bind를 사용할 수 있다.
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
setTimeout(this.printArea.bind(this), 2000)
}
화살표 함수를 이용해서도 바꿀 수 있다.
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
setTimeout(() => {this.printArea()}, 2000)
}
화살표 함수에서의 this는 화살표 함수 내에서 가지고 있는 this를 가르키게 된다 (스코프에 존재하는 this)
그리고 화살표 함수가 어떻게 사용되건 this는 바뀌지 않게된다.
이부분이 화살표함수의 실행은 this를 결정짓지 않는다는 말
(또는 화살표 함수는 스스로의 this를 가지지 않는다.)
때문에 많이 햇갈렸는데
정확히는 화살표 함수가 정의된 스코프에 존재하는 this를 가리키는 것이다.(lexical this)