JavaScript

3. Js의 this 정리

문정훈 2021. 10. 11. 13:41

1. Java에서 this와 super 키워드 

class Parent {
    protected int a = 10;
    protected int b = 20;
    
    protected Parent(int a) {
    	this.a = a;
    }
    
    public Parent() {}
}

class Children extends Parent {
    int a = 30;
    int c = 40;
    
    Children(int a) {
    	super(a);
    }
 
    public void method1 () {
    	System.out.println(this.a);
        System.out.println(super.a);
        
        System.out.println(this.b);
        System.out.println(super.b);
        
        System.out.println(this.c);
    }
}

자바에서는 this와 super 키워드가 "클래스" 내부에서 사용되는 키워드이다. 

클래스 내부에서 this 키워드는 해당 클래스 객체를 의미하며 super키워드는 상속하고 있는 부모 클래스를 가리킨다. 

a라는 변수는 Childer과 Parent 클래스 모두에 선언되어 있다.

 

  • Childeren 클래스에서 this.a는 Childeren 클래스의 a인 30을 가리킨다. super.a는 부모 클래스인 Parent의 a인 10을 가리킨다. 
  • this.b는 우선 Childern클래스에서의 b를 가리키는데 Childern클래스는 b라는 필드를 가지고 있지 않기 때문에 자신의 부모 클래스인 Parent 클래스에서 b의 값을 찾아 20을 가리킨다. super.b는 Childern 클래스의 부모 클래스인 b를 가리키므로 역시 20을 가리킨다. 
  • c변수는 Childern에만 선언되어 있고 Parent에는 선언되어 있지 않기 때문에 this.c은 40으로 할당된다. 
  • super 키워드는 함수의 형태로 호출하는 형태도 있는데, 자식 클래스 생성자에서 부모 클래스의 생성자를 호출할 때
    super() 호출을 하게된다. 

 

2. Js에서 this 키워드 

자바스크립트에서도 역시 this 키워드는 Js의 객체를 가리킨다. 하지만 자바 처럼 class 형태로 객체가 있을 뿐더러, Js에는 수많은 객체의 종류가 존재한다.

예를 들어 일반 함수는 일급 객체로 객체이며, 생성자 함수로 만들어진 객체, 객체 리터럴, 클래스 선언으로 생성된 클래스 등등이 있다. 

일반 함수, 생성자 함수, 객체 리터럴, 클래스에서 this키워드가 어떻게 작용하는지 정리해보자.

 

1) 객체 리터럴에서 this

person = {
	age : 20,
    
    getAge() {
    	return ${this.age};
    }
}

객체 리터럴에서 this는 그냥 자기 자신을 가리킨다. (쉽다.)

객체 리터럴을 선언한 소스 코드의 "평가"과정에서 실행 컨텍스트가 생성되면서 객체 리터럴의 this가 바인딩된다. 

 

2-1) 일반 함수 호출에서 this

var func1 = function() {
	console.log(this); //this는 window
}

var func2 = function() {
	function func3 () {
    	console.log(this); //this는 window
    }
}

var obj = {
    value : 1,
    function objFunc() { //객체 리터럴에 선언된 메소드
    	console.log(this); //obj에 바인딩	
    	function func4() {//메소드 안에 선언된 함수
        	console.log(this); //this는 window
        }
    }
}

일반 함수 역시 "일급 객체로" 객체이다. 그럼 일반 함수 호출에서 this를 사용하면 자신을 가리킬까?

=>아니다. 

일반 함수 호출에서 this는 기본적으로 전역 객체가 바인딩된다. func2에서와 같이 중첩 함수에서도 마찬가지이며, 일반 함수가 객체의 내부이던 중첩 함수이건, 콜백 함수이건 어떠한 경우에서든 선언되면, this는 window를 바인딩한다. 

객체를 생성하지 않는 일반함수에서는 this는 사용할 의미가 없기 때문에 사용하지 않으며 strict mode가 적용된 일반 함수 내부에서는 this는 undefined가 바인딩된다. 

 

결론=> 일반 함수에서는 this는 사용하지도 않고 값도 실제로 undefined를 가지는 경우가 일반적임

 

 

한 발 더 나아가 내용을 심화해보면

개념1) 메소드란?

js에서 함수와 메소드의 차이점을 보면 메소드란 객체에 바인딩된 함수를 일컫는 의미로 ES6에서는 메소드를 다음과 같이 정의한다. "메소드란 메소드 축약 표현으로 정의된 함수만을 의미한다."

위 정의를 좀 더 확장해서 정리해보면

  1. 우선 메소드는 non-constructor하다. (생성자 함수로 호출 불가능)
  2. prototype 프로퍼티가 없으며 프로토타입 역시 생성하지 않는다. 
var obj = {
  value : 1,
  objFunc() { //객체 리터럴에 선언된 메소드
    console.log(">>"+this); //obj에 바인딩	
    function func4() {//메소드 안에 선언된 함수
        console.log(">>"+this); //this는 window
      }
      func4();
  }
}

obj.objFunc()

//실행 결과
//>>[object Object]
//>>[object global]

위 코드를 다시 보면 objFunc은 함수가 아니라 메소드이다.

메소드에 선언된 this는 자신을 선언한 객체인 obj에 할당된다.

메소드 내부에 선언된 func4는 함수이다. 어떠한 경우(메소드 내부, 콜백함수, 중첩함수) 이건 함수에서의 this는 window에 바인딩된다. 

 

var o ={
  x : 1,
  foo() {
    console.log(this.x);
  },
  bar: function() {
    console.log(this.x);
  }
}

o.foo(); //1
o.bar(); //1

위 코드에서 foo는 메소드이다. 하지만 bar에 바인딩된 값은 함수이다. 메소드가 아니다. 

의문이 모든 함수의 this는 window에 바인딩되는데 o.bar()가 1로 출력되는 걸까??

코드의 평가 과정을 생각해보면 o라는 객체는 소스코드 평가 과정에서 객체가 heap 영역에 생성되고 

bar라는 key로 그 함수 값이 등록된다. 

그리고 런타임 시 o.bar()가 실행되면서 bar의 값으로 가지는 함수의 평가 과정이 진행되는데 이 평가 과정에서 자신을 호출한 o라는 객체에 동적으로 바인딩 되기 때문에 위와 같은 경우는 bar 의 값이 함수라 할지라도 this의 값이 o를 가리키는 것이다. 

 

 

2) 화살표 함수에서 this는?

화살표 함수는 콜백함수로 함수 선언문의 함수를 전달할시 해당 콜백함수의 this가 window에 할당된다는 문제점을 해결하기 위해 등장했다.

화살표 함수에서 this는 자신을 선언한 상위 스코프에 할당된다. 

 

 

 

 

 

3) 생성자 함수에서 this

사실 여기가 핵심.

function Circle(radius) {
	this.radius = radius;
}

Circle.prototype.getRadius = function () {
	return this.radius;
}

var cir1 = new Circle(10);

console.log(cir1.getRadius()); //10

Circle.prototype.radius = 20;

console.log(Circle.prototype.getRadius()); //20

생성자 함수에서는 this 바인딩이 함수의 "호출" 시점에서 this가 바인딩된다. 즉 함수가 호출되면 함수 코드의 "평가" 과정이 진행된다. 함수 코드의 평가 과정을 아래 순서로 간단히 정리했다.

지금은 this 바인딩이 언제 결정되는지만 보자!!

(자세한 과정의 내용은 "실행 컨텍스트" 글 참고)

  1. 함수 실행 컨텍스트 생성
  2. 함수 렉시컬 환경생성
    2-1. 함수 환경 레코드 생성  
      2-1-1. 객체 환경 레코드 생성
      2-1-2. 선언적 환경 레코드 생성
  3. this 바인딩
  4. 외부 렉시컬 환경에 대한 참조 결정

생성자 함수의 this 바인딩은 함수가 호출(함수 코드가 평가) 될 때 바인딩이 결정되며, 생성자 함수 메소드 내부의 this는 프로퍼티 값으로 메소드를 호출한 객체에 바인딩된다. 

 위 예시 코드를 보면 

 

첫 번째 console.log는 cir1 객체에서 getRadius 메소드를 호출하므로 getRadius 메소드 안의 this는 이 메소드를 호출한 객체인 cir1에 바인딩되고, 따라서 10을 가리키게 된다. 

 

두 번째 console.log는 Circle.prototype 객체에서 getRadius를 호출한다. 따라서 getRadius 메소드 안의 this는 이 메소드를 호출한 객체인 Circle.prototype이 바인딩되고, 따라서 20을 가리키게 된다. 

 

 

4) 클래스 선언에서 this 키워드

class Circle extends Shape {
	constructor(radius) {
        this.radius = radius
    }
    
    func() {
    	console.log(this.radius);
    }
}

Js에서 ES6부터 도입된 클래스의 기능은 자바와 같이 클래스 기반의 객체지향 프로그래밍 언어와 매우 비슷한 모습이다. 위 예제와 같이 Java에서 사용한 this방식이 거의 흡사하다.  

따라서 클래스에서 this키워드는 new 연산자로 생성한 클래스 객체를 바인딩한다.