[Javascript] - this


다른 언어를 하다가 javascript를 접하면 this를 보고 매우 당황하게 된다.

분명 다른 언어에서는 this, self 이런 용어들이 어렵게 다가오지 않았는데 javascript에서는 주의하지 않으면 의도하지 않은 동작을 하게 된다.

왜냐하면 javascript의 this는 동적으로 결정되기 때문이다.

정적? 동적?


먼저 javascript에서 스코프 체인에 대해서 정리해보자.

다른 컴파일 언어와 다르게 자바스크립트는 스크립트 언어, 인터프린터 언어이다.

그렇기 때문에 실행하면서 메모리 및 코드 평가 단계를 거치게 된다.

대표적으로 식별자라고 불리는 변수 선언은 javascript 코드가 코드 평가단계에서 렉시컬환경에 미리 선언 및 초기화를 진행하고 코드를 실행한다.

이 단계에서 스코프 체인을 만들게 된다.

스코프 체인은 코드 선언단계에서 결정됨으로 정적 스코프라고 부른다.

즉, 어디서 실행됬는가가 중요한게 아니라 어디서 정의되었는가가 중요하다.


var x = 5;

function foo() {
	var x = 4;
	bar();
}

function bar() {
	console.log(x); 
}

foo(); //5 foo의 x를 참조해서 bar에서 호출하지 않는다. 이것은 동적 스코프
bar(); //5

this는?


그런데 문제는 this는 동적 스코프이다. 즉, 정의된 영역이 중요한게 아니라 어디에서 실행되었는가가 중요하다.

일단 가장 기본적인 this 내용을 정리해보자.

this는 해당 객체 혹은 생성할 인스턴스를 가르킨다.

//해당 객체
const temp = {
	name: 'temp',
	func: function() {
		console.log(this.name);
	},
};

temp.func(); //temp => 해당 객체의 this로 name을 접근 가능하다.
//생성할 인스턴스
function Circle(radius){
	this.radius = radius  //생성될 인스턴스 객체를 의미한다.
	this.getArea = function() {...}
}

var circle = new Circle(5);
console.log(circle.radius); //5

문제는 예외 상황이다.

  1. 일반 함수 function() {…} 에서 this는 전역객체를 가르킨다.

     function Person() {
       // Person() 생성자는 `this`를 자신의 인스턴스로 정의.
       this.age = 0;
        
       setInterval(function growUp() {
         // 비엄격 모드에서, growUp() 함수는 `this`를
         // 전역 객체로 정의하고, 이는 Person() 생성자에
         // 정의된 `this`와 다름.
         this.age++;
       }, 1000);
     }
    
     function Person() {
       var that = this; //그래서 이런식으로 저장하여 해결했엇다.
       that.age = 0;
        
       setInterval(function growUp() {
         // 콜백은  `that` 변수를 참조하고 이것은 값이 기대한 객체이다.
         that.age++;
       }, 1000);
     }
    
  2. 화살표 함수는 prototype, constructor가 없다. this도 없다

    화살표 함수는 this가 없으므로 상위 객체로 스코프 체인을 타고 가서 결정하게 됩니다.

    즉, 생성되는 시점에 this가 결정되고 변하지 않습니다.

Object


예시 코드를 확인해보자.

function test() {
  const testData = {
    name: 'test',
    func: function () {
      console.log(this);
    },
    func2() {
      console.log(this);
    },
    func3: () => console.log(this),
  };

  testData.func(); //{name:'test',...}
  testData.func2(); //{name:'test',...}
  testData.func3(); //window
}

test();

일반 함수, 축약 메서드는 생성되는 object를 this로 가르킨다.

하지만 화살표 함수는 생성되는 상위 Object를 가르킨다.

다음을 확인해보자.

function test() {
  const testData = {
    name: 'test',
    obj: function () {
      return {
        name: 'test2',
        func: function () {
          console.log(this);
        },
        func2() {
          console.log(this);
        },
        func3: () => console.log(this),
      };
    },
  };

  const obj = testData.obj();
  obj.func(); //{name: 'test2', ...}
  obj.func2(); //{name: 'test2', ...}
  obj.func3(); //{name: 'test', ...}
}

test();

2중첩으로 obj 함수를 실행하면 새로운 객체를 생성해서 반환되게 수정하였다.

일반함수, 축약 메서드는 위와 똑같이 새로 생성되는 ‘test2’ 객체를 가르킨다.

하지만 화살표 함수는 생성되는 시점에서 자신을 생성하던 객체인 ‘test’ object를 this로 가르킨다.

class


class AAA {
  constructor() {
    this.name = 'AAA';
  }

  func = function () {
    console.log(this.name);
  };

  func2() {
    console.log(this.name);
  }

  func3 = () => console.log(this.name);
}

var aaa = new AAA();
aaa.func();
aaa.func2();
aaa.func3();

클래스에서는 일반함수, 축약 메서드, 화살표 함수 모두 자신이 생성한 객체 인스턴스를 가르킨다.

class AAA {
  constructor() {
    this.name = 'AAA';
  }

  func = function () {
    console.log(this.name);
  };

  func2() {
    console.log(this.name);
  }

  func3 = () => console.log(this.name);

  bbb() {
    return {
      name: 'BBB',
      func: function () {
        console.log(this.name);
      },
      func2() {
        console.log(this.name);
      },
      func3: () => console.log(this.name),
    };
  }
}

var aaa = new AAA();
aaa.func();  //AAA
aaa.func2(); //AAA
aaa.func3(); //AAA
const bbb = aaa.bbb();
bbb.func(); //BBB
bbb.func2(); //BBB
bbb.func3(); //AAA

위에 object에서 했던것을 클래스에서 반환하게 해보았다.

위 object에서 나온 결과처럼 일반 함수, 축약 메서드는 자신이 생성한 object를 this로 가지고

화살표함수는 자신을 생성해준 객체를 this로 가진다.

여기서 재미있는 장난을 쳐보겠습니다.

class AAA {
  constructor() {
    this.name = 'AAA';
  }

  func = function () {
    console.log(this);
  };

  func2() {
    console.log(this);
  }

  func3 = () => console.log(this);

  bbb() {
    return {
      name: 'BBB',
      func: function () {
        console.log(this);
      },
      func2() {
        console.log(this);
      },
      func3: () => console.log(this),
    };
  }
}

var aaa = new AAA();
const func = aaa.func;
const func2 = aaa.func2;
const func3 = aaa.func3;
func(); // undefined
func2(); // undefined
func3(); // AAA instance
const bbb = aaa.bbb();
const bfunc = bbb.func;
const bfunc2 = bbb.func2;
const bfunc3 = bbb.func3;
bfunc(); // undefined
bfunc2(); // undefined
bfunc3(); // AAA instance

위에서 말한대로 this는 생성되는 시점이 아니라 호출되는 시점에 결정됩니다.

그렇기 때문에 전역에서 다른 변수에 함수를 담고 실행하게 되면

실행하는 시점에 함수는 전역 변수에 담겨있기 때문에 this가 전역객체로 결정됩니다.

하지만 화살표 함수는 생성 시점에 결정되었기 때문에 변하지 않습니다.

정리


동적으로 결정되는 this인 만큼 더욱 조심히 써야하고

만약 this를 직접 결정해주고 싶다면 bind, call, apply함수를 사용해서 this에 객체를 할당해주면 됩니다.

그래도 앞으로 화살표 함수를 습관으로 들여서 특별한 경우가 아니면 화살표 함수를 쓰는게 더 안전하게 코딩할 수 있을것 같습니다.