Ecma Script 문법 (2)

Hazel_Song·2020년 10월 2일
0

WEB BASIC

목록 보기
4/6

지난 번에는 es6문법에서 비교적 개념 이해가 쉬우며, 코드 사용법에 대한 변화정도에 대해서 설명했었다.
이번에는 개인적으로 내가 es6문법을 공부할 때 가장 이해하기 어려웠던(지금도 정확하게 이해가 잘 되지 않는 다 사실...) this키워드, closure, 그리고 call, apply, bind에 대해서 정리해보고자 한다.

this 키워드

호출되는 함수가 실행될 때, 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

외부함수의 변수에 접근할 수 있는 내부변수이다.
즉 외부함수에서 사용된 변수가 내부함수에도 똑같이 사용되고 있다면 그 내부함수는 closure이다.
scope 체인과 연관된 개념이다. 즉 선언된 변수가 영향을 미치는 범위와 연관된 개념인 것이다.

function outer(){
  var a =1;
  function inner(){
    console.log(b)
  }
  inner();
}
outer();
profile
코드 한 줄로, 세상의 가치를 만들자🌟

0개의 댓글