지난 번에는 es6문법에서 비교적 개념 이해가 쉬우며, 코드 사용법에 대한 변화정도에 대해서 설명했었다.
이번에는 개인적으로 내가 es6문법을 공부할 때 가장 이해하기 어려웠던(지금도 정확하게 이해가 잘 되지 않는 다 사실...) this키워드, closure, 그리고 call, apply, bind에 대해서 정리해보고자 한다.
호출되는 함수가 실행될 때, this가 있다면, 이 키워드가 가르키는 특정 객체에 대해서 함수가 실행 될 것이다.
1. global
일반적으로 코드 전체에서 선언된 변수나, 일반 function내에서 선언된 this 키워드는 window를 가르킨다.
2. Method 호출
객체.메소드()형태로 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
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
3. new 키워드를 통한 생성자 호출
객체지향적인 성격을 지닌 JS를 사용할 때, 프로토타입과 인스턴스 생성을 많이하게 될 것이다.
차후에 공부하게 될 프로토타입 및 더 나아가서 리액트 컴포넌트를 공부하며서 계속 등장할 개념이다.
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
4.call, apply, bind
call, apply
함수(func)의 메소드들이다.
.call, .apply 호출은 명시적으로 this를 지정해서 실행하고 싶을 때 사용한다. 첫번째 인자가 항상 this값이 되어야 한다.
즉 첫번째 인자로, 함수가 불러올 때 this로 명시해서 가지고 오고 싶은 인자를 집어넣으면 된다.
//call과 apply는 기능은 유사하나, 사용법이 다를 뿐
function foo() {
return 'bar'
}
foo(1,2,3) //<-여기서 1,2,3은 인자이다.
foo.call(null,1,2,3)
foo.apply(null,[1,2,3])
//call은 넣고자 하는 인자들을 this 다음에 나열하고,
//apply는 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을 실행할 수 있습니다.
//객체 지향 프로그래밍에서 찾아볼 수 있는 예제
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
.bind는 .call과 유사하게 this 및 인자를 바인딩하나, 당장 실행하는 것이 아닌 바인딩된 함수에 대해 값을 단순히 리턴한다.
첫번째 인자는 this, 두번째 인자부터는 필요한 파라미터를 전달합니다.
fn.bind(this값, 인자1, 인자2, ...)
*bind의 실제 사용 사례
case 1: 이벤트 핸들러
이벤트 핸들러에서 이벤트 객체 대신 다른 값을 전달하고자 할 때
//예제1
<button id="btn">클릭하세요</button>
let btn = document.querySelector('#btn')
// 추후 이벤트에 의해 불리게 될 함수에서, this는 {hello: 'world'}가 됩니다.
btn.onclick = handleClick.bind({ hello: 'world'})
function handleClick() {
console.log(this)
}
//예제2
//bind 사용전
<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로 넘기거나, 혹은 인자로 보낼 수 있다.
//bind 사용후
//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)
}
case 2: setTimeout
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)
printAsync() {
// 1초 후 사각형의 넓이를 콘솔에 표시합니다
setTimeout(this.printArea.bind(this), 2000)
}
외부함수의 변수에 접근할 수 있는 내부변수이다.
즉 외부함수에서 사용된 변수가 내부함수에도 똑같이 사용되고 있다면 그 내부함수는 closure이다.
scope 체인과 연관된 개념이다. 즉 선언된 변수가 영향을 미치는 범위와 연관된 개념인 것이다.
function outer(){
var a =1;
function inner(){
console.log(b)
}
inner();
}
outer();