본문 바로가기
Woowa Techcourse/Missions

Javascript This

by mingule 2021. 4. 27.

This란 무엇일까!?

1. This가 나온 이유

객체는 State를 나타내는 property와 behavior를 나타내는 메서드를 하나의 논리적인 단위로 묶은 복합적인 자료구조다.

그니까 현재 나의 '상태'와 그 '상태'를 바꿔줄 수 있는 '행동'을 함께 가지고 있다고 이해하면 된다.

동작, 즉 behavior를 나타내는 메서드는 자신이 속한 객체의 Status, 즉 property를 참조하고 변경해줄 수 있어야 한다. '행동'이 '상태'를 변경할 수 있어야 한다는 거다!

보통 객체 리터럴 방식으로 생성한 객체의 경우, 내부 메서드에서 자신이 속한 객체를 가리키는 변수를 재귀적으로 사용할 수 있다. 

아래와 같이 circle을 circle 내부에서 사용할 수 있다는 것이다.

const circle = {
  radius = 5,
  getDiameter() {
   return 2*circle.radius
  }
}

그럼 이게 왜 가능할까?!

변수에 값이 할당되기 직전에, 우측의 식이 값으로 평가된다. 우리가 circle을 getDiameter() 안에서 사용하려면 미리 circle이 선언되어 있어야 하는데, getDiameter()가 호출되기 전에 이미 circle이 평가되어 객체가 생성되었고, 그렇기 때문에 우리는 getDiameter() 안에서 circle을 사용할 수 있다. 

 

그런데.. 이렇게 내부에서 내가 나를 부르는 재귀적인 방식은 가능하기는 하지만, 일반적이지 않다. 그리고 만약 생성자 함수 방식으로 Instance를 생성한다고 했을 때, 아직 만들어지지도 않은 Instance가 자신을 가리키는 식별자를 알 수 있을 리가 없다. 따라서, 자신이 속한 객체, 또는 자신이 생성할 인스턴스를 가리키는 특수한 식별자가 필요하다. 그렇기 때문에 This가 세상에 등장하게 되었다. 

 

2. 그래서 This가 뭐라고?!

This는 자신이 속한 객체, 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. 그냥 변순데, 내 자신을 가리키고 있다고 보면 된다. 우리가 함수를 호출하면, 함수에 전달될 parameter에 해당하는 arguments 객체와 this가 암묵적으로 함수 내부에 전달된다. 그렇기 때문에 this도 지역변수처럼 사용할 수 있다. 

단, this가 가리키는 값(this binding)은 함수 호출 방식에 의해 동적으로 결정된다.

 

3. This binding은 또 뭐야?

구글 번역기

bind라는 단어의 뜻은, 묶다, 굳히다, 둘러감다... 뭐 이런 뜻들이 있다. 그러니까 this를 묶어주는 거라고 생각하면 된다. 

this는 앞서 말했듯, 함수 호출에 따라 값이 동적으로 변한다. 그렇기 때문에 This binding은 this와 this가 가리킬 객체를 묶어주는 거라고 보면 된다.

 

렉시컬 스코프와 This binding은 결정 시기가 다르다.

렉시컬 스코프 - 함수의 정의가 평가되어 함수 객체가 생성되는 시점에 상위 Scope를 결정

This binding - 함수가 호출되는 시점에 결정 

 

1. 일반 함수 호출

This는 객체의 Property나 Method를 참조하기 위한 자기 참조 변수이므로, 일반적으로 객체의 메서드 내부나 생성자 함수 내부에서만 의미가 있다. 그냥 일반 함수의 내부에서는 This를 사용할 필요가 없다. 만약 일반 함수의 내부에서 this를 사용하게 된다면, 전역 객체인 window가 바인딩된다. 엄격 모드, strict mode에서는 일반 함수에서 this를 사용하면 undefined가 뜬다. 

 

'어떤 함수라도' 일반 함수로 호출되면 this에 전역 객체가 바인딩된다. 

이런 바인딩을 피하기 위해서는 원하는 this를 다른 변수에 담아 전달해주는 방법과, 화살표함수를 사용해 this binding을 일치시키는 방법이 있다. 

 

2. 메서드 호출

메서드 내부의 this에는 메서드를 호출한 객체가 바인딩된다. 예를 들어 car.getName() 이 호출되었을 때, car가 this에 bind된다는 것이다. 

그런데 여기에서 알아두어야 할 점은, 우리가 car 안에 getNname 이라는 메서드를 만들었다고 해서 getName이 가리키고있는 함수까지 car 안에서만 존재하는건 아니다. 

앞의 예시를 다시 활용해 말하자면, 

circle 안의 getDiameter라는 메서드는 어떤 독립적으로 존재하는 { return 2*this.radius }라는 객체를 가리키고 있는 것이지, 이 메서드 자체가 { return 2*this.radius } 라는 객체를 가지고 있는 것이 아니라는 것이다. 

const circle = {
  radius = 5,
  getDiameter() {
   return 2*this.radius
  }
}

그렇기 때문에, getName property가 가리키는 함수 객체인 getName 메서드, 즉 { return 2*this.radius } 는 다른 객체의 메서드가 될 수도 있고, 일반 변수에 담아 일반 함수로 호출될 수도 있다. 

const circle = {
  radius = 5,
  getDiameter() {
   return 2*this.radius
  }
}

// newCircle 객체에 getDiameter 할당 
const newCircle = {
  radius = 7
}
newCircle.getDiameter = circle.getDiameter;

// getName 메서드를 변수에 할당
const getDiameter = circle.getDiameter
// 일반 함수처럼 호출 가능
getDiameter()

일반 함수처럼 호출되면 this는 window에 바인딩 될 것이고, newCircle에서 호출되면 this 는 newCircle을 가리킬 것이다. 

 

3. 생성자 함수 호출

생성자 함수의 This는 생성자 함수가 미래에 생성할 인스턴스를 가리킨다. 일반 함수처럼 만들고 앞에 new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작한다. new를 꼭 붙여야 생성자 함수로 동작하기 때문에 잊지 말즈아!!

생성자 함수에서 this가 할당되는 과정이다. 그래도 알고 지나가면 좋을 것 같다.

function User(name) {
 // this = {};

 // 새로운 프로퍼티를 this에 추가
 this.name = name;
 this.isAdmin = false;
 
 // return this; (this의 암시적 반환)
 }

 

4. Function.prototype.apply/call/bind 메소드에 의한 간접적인 호출

Function.prototype의 메서드기 때문에 모든 함수가 상속받아 사용할 수 있다.

Call, Apply는 기본적으로 함수를 호출하는 메서드다. 이 두가지 메서드는 함수를 호출하면 첫 번째 인수로 전달한 특정 객체를 호출한 함수의 this에 바인딩한다. 두 가지는 인수를 전달하는 방식만 다를 뿐, 동일하게 동작한다. 

그러니까, this를 전달해주면서 호출까지 해주는 메서드들이라고 보면 된다.

// Call
Function.prototype.call(thisArg[, arg1[, arg2[, .. ]]]])

function getThisBinding() {
  return this;
}

const thisArg = {a:1}

console.log(getThisBinding()) // window

console.log(getThisBinding.call(thisArg, 1,2,3 )) // {a:1}

// Apply
Function.prototype.call(thisArg[, arg1[, arg2[, .. ]]]])

function getThisBinding() {
  return this;
}

const thisArg = {a:1}

console.log(getThisBinding()) // window

console.log(getThisBinding.call(thisArg, 1,2,3 )) // {a:1}

이친구들의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에 배열 메소드를 사용하는 경우다. 

그니까 배열처럼 생긴 배열은 아닌 넘들에게 배열 메소드를 사용할 수 있게 만들어준다. 

function listCall() {
  return Array.prototype.slice.call(arguments)
}

let list1 = listCall(1, 2, 3) // [1, 2, 3]

function listApply() {
  return Array.prototype.slice.apply(arguments)
}

let list1 = listApply(1, 2, 3) // [1, 2, 3]

call과 bind를 함께 활용할 수도 있다.

 

let unboundSlice = Array.prototype.slice
let slice = Function.prototype.call.bind(unboundSlice)

function list() {
  return slice(arguments)
}

let list1 = list(1, 2, 3) // [1, 2, 3]

반면, bind는 this만 전달해줄 뿐, 함수를 호출하지는 않는다. 간단한 예시로 Callback함수를 들 수 있겠다.

const person = {
  name: 'mingule',
  foo(callback) {
    setTimeout(callback, 100);
  }
}

person.foo(function() {
  console.log(`hi! this is ${this.name}`)
})

위의 예시는 callback으로 일반 함수를 호출했기 때문에 this가 전역 객체이다. this를 person의 this로 바꾸어주려면, 아래와 같이 bind를 해주면 된다!

const person = {
  name: 'mingule',
  foo(callback) {
    setTimeout(callback.bind(this), 100);
  }
}

person.foo(function() {
  console.log(`hi! this is ${this.name}`)
})

 

댓글