TIL this

백광호·2021년 1월 12일
0

TIL

목록 보기
33/55

코드스테이츠 37일차

공부하는 방법에 대한 글을 코드스테이츠 깃허브에서 보게 되었다.
내게 부족한것이 무엇인지 생각하면서 공부하는 방법이었다.
글을 보고 자극받아 내 블로그에도 내가 알아둬야할 것들이 무엇인지
추가해 보기로 했다.

새로 배운 것들

this

함수를 호출하는 방법 중 하나로 함수 호출 시 5가지 바인딩 패턴이 존재한다.
아래 표는 바인딩 패턴에 대한 설명이다.

패턴바인딩되는 객체설명
Mathod 호출부모 객체(실행 시점에 온점 왼쪽에 있는 객체)하나의 객체에 값과 연관된 메소드를 묶어서 사용할 때 주로 사용함
new 키워드를 이용한 생성자 호출새롭게 생성된 인스턴스 객체객체 지향 프로그래밍에서 주로 사용함
.call 또는 .apply 호출첫번째 인자로 전달된 객체this값을 특정할 때 사용하며, 특히 apply의 경우 배열의 엘리먼트를 풀어서 인자로 넘기고자 할 때 유용함

위 세가지 바인딩 패턴은 꼭 알아둬야한다.

패턴바인딩되는 객체(브라우저)바인딩되는 객체(node.js)
Globalwindow (strict mode 에서는 undefined)module.exports
Function 호출window (strict mode 에서는 undefined)global

위 두가지 패턴은 this를 사용하지 않는 것을 권장한다.
함수 호출 시 this를 사용할 이유는 없기 때문이다.

경우에따라 바인딩이 달라지지만 굳이 외울필요는 없고
사용하지 않으면 된다.

Method 호출

메소드 호출은 객체.메소드()와 같이 객체 내에 메소드를 호출하는 방법을 말한다.

아래 예제를 살펴보면서 어떻게 작동하는지 설명해보자

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.valuecounter1.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

이런식으로 사용할 경우 thismakeCounter()가 된다.

생성자 호출

생성자 호출은 객체.메소드()와 같이객체 내에 메소드를 호출하는 방법과 비슷하지만,
객체가 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

카운터를 클래스로 만들어보자
클래스를 만드는 방법은 여기서 확인

이 때 thisnew Counter()로 생성한 인스턴스가 된다.
클래스를 만들 때 this.value = 0 이라고 사용할 데이터를 정의하고
new Counter()로 객체를 호출한다.

call, apply 호출

.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입니다.

bind

.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)

알아둬야할 것들

  • git 명령어
  • 리팩토링
  • 알고리즘
profile
안녕하세요

0개의 댓글