this의 동적 바인딩

정호진·2022년 9월 28일
0

목록 보기
3/7

this?

혹시 JavaScript의 this에 대해 알고 계시나요? 흔히 우리가 알고 있는 this는 '자기 참조 변수(self-refenrence variable)'. 즉 자신 자신을 가리키는 변수입니다. 만약 전역 컨텍스트에서 this를 호출하게 된다면 window객체가 호출되게 됩니다. 최상위 객체이기 때문입니다.

그렇다면 다음 문제를 통해 참조되는 this값을 예상해 봅시다.

const o = {
  name: "Kim",
  changeMyName: function (name) {
   this.name = name 
  },
};

const o2 = {
  name: "Song",
};

function callFuncWithArg(f, arg) {
  f(arg);
}

function Person(name){
  this.name = name
}

const p = new Person('Jeong')

o.changeMyName.bind(o2)("Sam");
console.log("1번 - ", o2.name); // Sam
callFuncWithArg(o.changeMyName, "Daniel");
console.log("2번 - ", o.name); // Kim
o.changeMyName("Sam");
console.log("3번 - ", o.name); // Sam
console.log('4번 - ', p.name) // Jeong

답은 위에 나와있는 코드와 같이 나타나게 됩니다. 그렇다면 왜 다음과 같은 경우가 나타나게 되는 것일까요? 우리가 this에 대해 알아야 할 중요한 특징 중 하나는 "this가 가리키는 값은 함수 호출 방식에 따라 결정된다는 것" 입니다.
this가 결정되는 방식에는 크게 4가지가 있습니다.

1. 일반 함수 호출

기본적으로 전역 객체 window가 바인딩 됩니다. 하지만 평소 this의 용도를 생각하면 일반 함수에서 this는 의미가 없습니다. strict mode에서는 undefined가 바인딩 됩니다.

콜백 함수가 일반 함수로 호출된다면 콜백 함수 내부의 thiswindow가 바인딩 됩니다. 어떤 함수라도 일반 함수로 호출한다면 thiswindow가 바인딩 됩니다.

위와 같은 예시는 위 코드에서 2번 호출을 통해 알 수 있습니다. 2번을 보면 함수 실행할 콜백 함수와 그 인자를 매개변수로 전달합니다. 하지만, callFuncWithArg라는 일반 함수 호출이 되기 때문에 전달받은 o.changeMyName에서 가리키는 this는 객체 o가 아닌 window가 됩니다. (실제로 window.name에 Daniels라는 변수가 저장되어 있습니다.)

이처럼 보조 함수의 this와 메서드 내의 this가 다른 것을 가리키는 것은 메서드 동작에 있어서 잘못된 오류를 발생시키기 충분합니다. 이를 해결하기 위해서 화살표 함수를 사용하거나 call/bind/apply 메서드를 통해 this를 바인딩 하여 사용합니다.(1번 방식 참조)

2. 메서드 호출

메서드는 객체_이름. 메서드_이름의 형식으로 호출됩니다. 여기서 주의해야 할 점은 메서드 내부this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩 된다는 것입니다.

	const person = {
  name: 'Lee',
  getName(){
      return this.name
      }
  }

  const anotherPerson = {
      name:'Jeong'
  }

  anotherPerson.getName = person.getName

  const getName = anotherPerson.getName 

  //앞에서도 언급했듯이 일반함수 호출은 this에 window를 바인딩 한다. 즉 window.name을 호출하는 것이다.
  console.log(anotherPerson.getName(),person.getName(),getName()) // Jeong Lee ''

위 코드를 보면 메서드 내부의 this는 프로퍼티 메서드를 가리키고 있는 객체와 관계없이 메서드를 호출한 객체에 바인딩 됩니다. person객체의 프로퍼티가 가리키는 함수 각체는 person객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체입니다.

이는 prototype 메서드 내부에서 선언한 this에도 똑같이 적용이 됩니다.

const person = {
  name: 'Lee',
  getName(){
    return this.name
  }
}

const anotherPerson = {
    name:'Jeong'
}

anotherPerson.getName = person.getName

const getName = anotherPerson.getName 

//앞에서도 언급했듯이 일반함수 호출은 this에 window를 바인딩 한다. 즉 window.name을 호출하는 것이다.
console.log(anotherPerson.getName(),person.getName(),getName()) // Jeong Lee ''

생성자 함수를 통해 생성된 인스턴스 me가 가리키는 name은 생성자에서 선언한 Jeong이며, Person.prototype 객체가 가지고 있는 nameKim입니다.

3. 생성자 함수 호출

생성자 함수 내부의 this는 생성자 함수가 생성한 인스턴스가 바인딩 됩니다. (3번 예시)
p라는 인스턴스에 new 키워드를 통해 생성자 함수를 호출하게 된다면 this가 가리키는 것은 p가 됩니다. 하지만, new키워드를 사용하지 않고 호출한다면, 일반 함수와 동일하게 동작합니다.

4. apply/call/bind에 의한 간접 호출

앞서 언급했듯이, 콜백 함수의 this와 생성자의 this가 다른 것은 큰 혼란을 야기하게 됩니다. 이를 해결하기 위한 방법이 apply/call/bind입니다. 해당 메서드를 통해 호출하는 함수의 this를 원하는 객체로 지정할 수 있습니다.

/*
	@param thisArg - this로 사용할 객체
	@param argsArray - 함수에게 전달할 인수 리스트의 배열 또는 유사 배열 객체
	@returns 호출된 함수의 반환값
*/
Function.prototype.apply(thisArg[,argsArray])
/*
	주어진 this 바인딩과 ,로 구분된 인수 리스트를 사용하여 함수를 호출한다.
	@param thisArg - this로 사용할 객체
	@param arg1,arg2, ... - 함수에게 전달할 인수 리스트
	@returns 호출된 함수의 반환값
*/
Function.prototype.call (thisArg[,arg1[,arg2[,...]]])

applycall 메서드는 함수를 호출하는 것입니다. 두 메서드 모두 함수를 호출하면서 첫 번째 인수로 전달한 객체를 호출한 함수의 this에 바인딩 합니다. 두 메서드는 인수 전달 방식만 다를 뿐이지 같은 기능을 동작합니다.
반면에 bind 메서드는 함수를 호출하지 않고 this로 사용할 객체만 전달합니다.

function getThisBinding(){
    return this
}

const thisArg = {a:1}

console.log(getThisBinding.bind(thisArg)) // getThisBinding
console.log(getThisBinding.bind(thisArg)()) //{a:1}

이처럼 bind메서드는 메서드의 this와 메서드 내부의 중첩 함수 또는 콜백 함수의 this가 불일치하는 문제 해결을 위해 사용됩니다. (1번 참조)

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

person.foo(function(){
    console.log(this.name) // ' '
})
// 일반함수로 호출된  콜백함수의 this는 전역객체를 바인딩하기 때문에 아무것도 의도한 대로 this.name이 반환되지 않는다.

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

person.foo(function(){
    console.log(this.name) // Jeong
})

맺으며

오랜만에 this에 대해 공부하다가 제일 위에 있는 문제의 2번 답을 보고 "아니 왜 Daniels가 아니지?"라는 의문에 다시 한번 공부하게 되었습니다. 예전에 책을 보면서 정리를 했다고 생각했던 개념이었지만, 사용하지 않고 얕게 훑기만 해서 그런지 처음에는 정말로 왜 그런지 몰랐습니다. 그리고 다시 한번 공부를 하니 훨씬 이해가 잘 된 개념이었습니다.
혹시 잘못된 부분이 있다면 말씀 부탁드립니다!


참조

JavaScript Deep Dive

0개의 댓글