This

devAnderson·2022년 4월 5일
0

TIL

목록 보기
84/105

0. 시작하기 앞서

사실상, 함수형 프로그래밍에 익숙하고 리엑트의 함수 컴포넌트가 점차 일반화되는 시점에서 This를 보게되는 상황은 매우 드물다고 할 수 있다.
하지만, 그렇다고 하여 객체 지향적 개발이 레거시가 된 것도 아닐 뿐더러 angular나 일부 객체지향적 데이터베이스(ex, typeORM) 을 살펴보는 경우가 생길 경우 this를 마주하게 되는 것은 필연에 가까우며 면접에서도 상당히 자주 물어보기 때문에 정리한다는 개념으로 작성하여본다

🚍 1. this의 의미

객체 지향적 개발에서 객체란 크게 두가지의 형태를 가진다
1. 상태 : 객체 내부에 존재하는 상태 프로퍼티들은 그 객체가 지니는 고유의 데이터이다
2. 동작 : 객체 내부에 존재하는 메서드들은 그 객체의 상태를 CRUD 할 수 있는 함수이다

이때, this가 관여되는 부분이 바로 "동작" 에 해당하는 메서드이다.

이 동작에 관련된 메서드는 상태를 변경하기 위해서 필수적으로 이 상태 프로퍼티를 포함하고 있는 객체에 접근할 수 있어야 한다.

물론, 객체자체가 리터럴로 형성되어 있는 경우라면 그냥 객체에 바인딩되어있는 식별자를 참조하면 된다.

const obj = {
   state : 1,
   action() {
      obj.state += 1;
   }
}

obj.action(); 
// 이 메서드가 호출되는 시점에는 이미 obj의 식별자 정의가 완료되고, 
//이 식별자에 평가완료된 객체 리터럴의 레퍼런스 주소가 바인딩된 상태이기 때문에 동작이 가능하다.

그러나, 객체 지향적 개발의 가장 큰 기능은 바로 생성자 함수를 통한 상속구현이 완료된 객체를 생성하는 것에 그 의의가 있다.

function Obj (stateValue) {
   인스턴스.state = stateValue;
}

Obj.prototype.action = function(){
   인스턴스.state += 1;
}

const obj = new Obj(1);
// 이 코드가 실행될 당시의 실행 컨텍스트를 생각해보자
// function Obj는 함수객체로 평가가 된 상태고, 이 객체의 prototype에 상속가능한 프로토타입 메서드 "action"이 할당된 상태이다.
// new 문이 실행되면 뒤에 정의된 생성자 함수를 호출하면서 암묵적으로 임시 인스턴스 객체를 만들어 메모리에 저장한 후, 그 객체 내부를 
// 생성자 함수의 정의에 따라 초기화시켜야 한다.
// 여기서 문제점은, 방금 전 object literal 때와는 다르게 생성자 함수가 미래에 만들어질 인스턴스를 초기화시키기 위해서는
// 필수적으로 이 미래에 생성될 인스턴스를 지칭할 식별자가 필요하다. 여기서 바로 this의 필요성이 존재하는 것이다.

다시 말해서 this란 생성자 함수가 인스턴스를 초기화 할 때에 해당 생성될 인스턴스를 지칭하기 위한 "자기 참조 변수"이다.

🚍 2. this의 바인딩

함수가 호출이 되는 순간 만들어지는 실행 컨텍스트에는 자동으로 arguments라는 유사배열 객체와 this 가 정의되게 된다.
이때 this가 정의되는 방식은 동적으로 결정되므로 구분해서 알아둬야 한다.

a. 일반함수로 호출될 경우

일반함수로 호출되는 경우, 해당 함수 실행 컨텍스트에 바인딩되는 this는 전역객체인 window가 된다.

function global () {
 console.log(this);
}

global() // window객체

단, 해당 코드가 strict mode에서 실행될 경우, this는 undefined를 가리키게 된다.
setTimeout과 같은 비동기 함수 호출 내의 콜백함수에서 this가 정의되었다 하더라도, 일반함수 호출과 동일한 프로세스를 거친다.
왜냐하면 콜백 함수가 테스크 큐에 있다가 콜스텍이 비워지면 그대로 들어가서 일반함수로 실행되기 때문

b. 객체 메서드로서 호출될 경우

const obj = {
   action(){
     console.log(this);
   }
}

obj.action() // obj 객체

이 경우, 메서드로 호출되는 순간 콜스텍에 저장되는 메서드의 실행 컨텍스트 내부 this에는 함수 객체가 바인딩되어 정의되는 상태이다.
만약, prototype에 정의되어 있는 메서드가 호출되는 경우, 이때 this는 자신을 호출한 객체인 prototype 객체가 된다.

function Test () {
   this.name = "a";
}

Test.prototype.name = "b";

Test.prototype.getName = function () {
   console.log(this.name);
}

const obj = new Test();
obj.getName(); // "a"
Test.prototype.getName(); // "b"

c. 생성자 함수로서 호출될 경우

이때 생성자 함수의 실행 컨텍스트 내부에 전달되는 this에는 new 명령문이 암묵적으로 생성하는 인스턴스의 주소에 바인딩되게 된다.
참고로 이 인스턴스 내부구조는 아래와 같다

function Construction(){
   this.name = "hello"
}

const obj = new Construction(); 
// 이때 생성되는 임시 인스턴스 내부에는 [prototype] 슬롯이 존재한다
// 이 슬롯 내부에 바인딩되는 객체에는 "constructor" 프로퍼티가 존재하고, 이것은 생성자 함수를 가리킨다 (Construction)

d. bind/ call/ apply 로 호출될 경우

함수 객체의 prototype 슬롯에 존재하는 Function.prototype 객체에는 생성자 함수 Function으로부터 상속받아오는 다양한 프로토타입 메서드가 존재한다.

이때 함수가 호출될 때 당시에 실행 컨텍스트에 던져질 this를 개발자가 직접 결정이 가능한 bind, call, apply 메서드가 존재한다.

간단하게 구분하자면

bind는 함수의 this가 변경된 함수를 리턴하고,
apply와 call은 인자로 전달받는 대상을 this와 바인딩시키면서 타겟 함수를 호출한다.
apply는 두번째 인자로 배열이 전달되며, 이 자체를 arguments에 할당하고
call은 두번째로부터 나열되는 인자들을 차례대로 arguments의 프로퍼티 값으로 바인딩하며 함수를 호출하는 차이가 있다.

bind의 구조는 클로져의 개념으로 구현하면 아래와 같다.

// 1. 일반 함수를 정의한다.
function test () {
 return this // 이대로 test를 호출하면 strict mode일 경우 undefined가 된다
}

// 2. test 함수객체의 prototype에 bind를 정의한다. ( 함수객체의 prototype 프로퍼티 객체 내부 [Function.prototype] 내부 메서드 bind를 오버라이딩 하는 용도 )
test.prototype.bind = function (thisTarget) {
    return (...args) => this.apply(thisTarget, args); 
    // 해당 bind 함수 내에서 바인딩되는 this는 함수객체 test이다.
    // apply의 정의에 따라 첫째 인자로 this가 될 대상을, 둘째 인자로 함수가 호출되면서 전달할 arguments 대상의 배열을 전달하면 된다.
}

const parentProto = {name : "Anderson"};

const updatedFunc = test.bind(parentProto);  // (...args) => this.apply(thisTarget, args); 

spread 문법이 생기기 전까지는 갯수가 정해지지 않은 인자들을 다뤄야 할 때 arguments 유사배열객체에 함수 prototype 메서드를 사용하기 위하여 apply와 call이 이용되곤 하였다.

function coverArgs (){
   const arr1 = Array.prototype.slice.call(arguments); // Array 의 프로토타입 메서드들은 this가 해당 호출대상인 배열이므로 arguments와 같은 유사배열객체가 해당 메서드를 사용하려면 이렇게 call을 통해 this로 전달하면서 사용하면 된다.

   const arr2 = [...arguments].slice(); // es6 스프레드 문법 업데이트 이후는 이렇게 간결하게 사용 가능하다.
}

화살표 함수의 경우

위와 같은 복잡한 this의 케이스 때문에, es6 이후로 도입된 화살표 함수에는 this바인딩이 존재하지 않고, 항상 상위 실행 컨텍스트의 this를 참조하게 된다.

const value = 100;

const obj = {
   foo (){
      setTimeout(()=> console.log(this), 100) 
   }
}

obj.foo() // obj
// 위의 경우, 화살표 함수가 아니라면 setTimeout의 콜백함수는 일반함수호출이 되므로 this는 window가 될 것이다.
// 그러나 화살표 함수는 무조건 상위 컨텍스트에 정의된 this를 사용하므로, 이때의 this는 foo함수의 this, 즉 obj가 된다.
profile
자라나라 프론트엔드 개발새싹!

0개의 댓글