본문 바로가기
JavaScript

클래스 (2)

by oncerun 2022. 3. 3.
반응형

 

클래스를 더 공부하기 전 프로퍼티 어트뷰트를 공부하고 다시 돌아왔다.

 

클래스의 접근자 프로퍼티를 이해할 수 있다.  접근자 프로퍼티의 디스크립터 객체를 보면 다음과 같이 구성되어 있다.

 

get ([[Get]]) set ([[Set]] enumerable ([[Enumerable]] configurable ([[Configurable]])

()는 내부슬롯

 

get과 set은 접근자 함수로 구성되어 있다. 이러한 접근자 프로퍼티는 클래스에서도 사용할 수 있다.

 

class Person{

	constructor(name){
		this.name = name;
	}

	get getName(){
		return this.name;
    }

	set setName(name){
		this.name = name;
    }
}

 

이때 getter와 setter는 인스턴스 프로퍼티처럼 사용이 된다.  이는 호출하는 것이 아닌 프로퍼티처럼 참조하는 형식으로 사용하며, 참조 시에 내부적으로 getter, setter가 호출된다.

 

클래스의 메서드는 기본적으로 프로토타입 메서드가 된다. 따라서 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.

 

 

클래스 필드 정의

 

클래스 필드란 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어이다. 

 

자바를 경험했다면 자바의 클래스에서는 클래스 필드(멤버 변수)는 클래스 내부에서 변수처럼 사용할 수 있으며 this는 암묵적으로 클래스가 생성할 인스턴스를 가리킨다. 

 

하지만 자바스크립트의 클래스 몸체에서는 메서드만 선언할 수 있고, 몸체에 클래스 필드를 선언하면 문법 에러가 발생했다. 하지만 TC39 프로세스인 ECMA-262 사양에 제안되어 있고 이제 안을 구현체들이 미리 구현해 놓았기 때문에 미리 경험을 할 수 있다.  (구현체란 타입 스크립트, Node.js, 크롬들의 특정 버전)

 

자바스크립트의 this는 클래스의 constructor와 메서드 내부에서만 유효하다.

자바에서는 this를 생략해도 문제가 없지만 자바스크립트에서는 생략할 수 없기에 반드시 this 키워드를 사용해야 한다.

 

(여기서 살짝 의문이 들었다. 앵귤러에서 클래스 필드가 없이 constructor에서 this. 변수로 값을 초기화하려면 에러가 발생했다. 그래서 매번 클래스 필드를 정의하고 생성자에서 this. 필드에 값을 초기화해주었는데, 자바스크립트에서는 클래스가 생성할 인스턴스에 클래스 필드에 해당하는 프로퍼티가 없다면 자동으로 추가된다고 알고 있기 때문이다. )

 

 -> ts파일 즉 타입 스크립트를 사용한다면 에러가 발생한다. 따라서 클래스 몸체에 클래스 필드를 사전에 선언하고 생성자에서 초기화를 해야 한다.

 

 

 

만약 자바스크립트에서  private 필드를 정의하려면 어떻게 해야 할까. 이도 클래스 몸체에 필드를 정의하는 제안처럼 

private 필드 또한 TC39 프로세스 stage 3에 표준 사양이 제안되어 있다. 따라서 우리는 구현체들에서 활용할 수 있다.

 

자바스크립트의 private 필드의 선두에는 #을 붙여준다. private 필드를 참조할 때도 #을 붙여주어야 한다.

 

다만 타입 스크립트는 클래스 기반 객체지향 언어가 지원하는 접근 제한자를 모두 지원하며 의미 또한 동일하다.!

 

 

 

자바스크립트에서도 extends 키워드를 통해 클래스 간 상속이 가능하다. 

이 부분에서 흥미로운 점은 extends 뒤에 [[Constructor]]를 가진 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다는 점이고 이는 동적으로 상속 대상을 결정할 수 있다는 점이다. 

 

사실 extends를 사용해도 클래스는 프로토타입을 통해 상속 관계를 구현한다. 이 슈퍼클래스와 서브클래스는 인스턴스의 프로토타입 체인뿐 아니라 클래스 간의 프로토타입 체인도 생성하기 때문에 프로토타입 메서드, 정적 메서드 모두 상속이 가능하다.

 

 

자바에서 상위 클래스의 생성자를 호출하기 위해 super키워드를 사용하는 것과 같이 자바스크립트에서도 super키워드가 존재한다.

 

다만 주의사항을 기억해야 할 것 같다.

 

  1. 서브클래스에서 constructor를 생략하지 않는 경우 서브 클래스의 생성자에서는 반드시 super를 호출해야 함 
  2. 서브클래스의 생성자에서 super를 호출하기 전에는 this를 참조할 수 없음.(부모가 정의돼야 자식이 정의됨)
  3. super는 반드시 서브 클래스 생성자에서만 호출하고 다른 곳에서는 호출할 수 없다.
  4. 메서드 내에서 super를 참조하여 슈퍼클래스의 메서드를 호출할 수 있다. 
    super 참조를 통해 슈퍼클래스 메서드를 참조하기 위해선 super가 슈퍼클래스의 메서드가 바인딩된 객체인 prototype 프로퍼티에 바인딩된 프로토타입을 참조할 수 있어야 한다.
class Base{

	constructor(name){
    	this.name = name;
    }
    
    sayHi(){
    	return `Hi! ${this.name}`;
    }
}


class Derived extend Base{

	sayHi(){
    	//클래스의 프로토타입이 수퍼클래스를 가르킨다.
    	const __super = Object.getPrototypeOf(Derived.prototype);
        return `${__super.sayHi.call(this)} how are you?`;
    }
}

 

super는 자신이 참조하고 있는 메서드가 바인딩되어있는 객체(Derived.prototype)의 프로토타입(Base.prototype)을 가리킨다. 따라서 super.sayHi는 Base.prototype.sayHi를 가리킨다. 단 super.sayHi를 호출할 때 call 메서드를 사용해 this를 전달해야 한다. 

만약 전달하지 않고 Base.prototype.sayHi를 그대로 호출하면 sayHi 메서드 내부의 this는 Base.prototype을 가리킨다. 

 

sayHi메서드는 프로토타입 메서드이기 때문에 인스턴스를 가리키게 해야 한다. name 프로퍼티는 인스턴스에 존재하기 때문이다. 

 

이와 같이 super 참조가 동작하기 위해서는 super를 참조하고 있는 메서드인 sayHi가 바인딩되어 있는 객체(Derived.prototype의 프로토타입인 Base.prototype)를 찾을 수 있어야 한다. 이를 위해 메서드는 내부 슬롯 [[HomeObject]]를 가지며, 자신을 바인딩하고 있는 객체를 가리킨다.

 

헷갈린다 다시 한번 정리해보자.

  • [[HomeObject]]는 메서드의 내부 슬롯이고 자신을 바인딩하고 있는 객체를 가리킨다.
  • [[HomeObject]]는 자신을 바인딩하는 객체이기 때문에 이 객체를 통해 객체의 프로토타입을 찾을 수 있다. 
    ( 클래스의 메서드는 프로토타입 메서드이기 때문에)
    여기선 Derived 클래스의 sayHi메서드의 [[HomeObject]]는 Derived.prototype을 가리킨다. 
  • 이를 통해 sayHi메서드 내부의 super 참조가 Base.prototype으로 결정되는 것이다. 
  • 따라서 super.sayHi는 결국 Base.prototype.sayHi를 가리킨다.

* ES6의 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]]를 갖는다. 이 [[HomeObject]]를 가지는 함수 만이 super참조를 할 수 있다. 이는 서브 클래스의 메서드에서 사용해야 함을 뜻한다. 

 

그렇다면 상속 클래스의 인스턴스의 생성과정은 어떻게 될까

 

1. 서브클래스의 super 호출

 

 자바스크립트 엔진은 클래스를 평가할 때 슈퍼 클래스와 서브 클래스를 구분하기 위해 "base" 또는 "derived"를 값으로 갖는 내부 슬롯[[ConstructorKind]]을 갖는다. 

 

다른 클래스를 상속받지 않는 클래스와 생성자 함수는 내부 슬롯 [[ConstructorKind]]의 값이 "base"로 설정되지만 

다른 클래스를 상속받는 서브클래스는 내부 슬롯의 값이 "derived"로 설정된다. 

이를 통해 new 연산자와 함께 호출되었을 때 동작이 구분된다.

 

[[ConstructorKind]]의 값이 "base"인 경우에는 클래스의 인스턴스 생성과정과 동일하게 암묵적인 빈 객체 생성 후 이를 this에 바인딩한다.

하지만 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 슈퍼클래스에게 인스턴스 생성을 위임한다. 

이것이 바로 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유다.

 

서브클래스가 new 연산자와 함께 호출되면 서브클래스 constructor 내부의 super 키워드가 함수처럼 호출된다.

super가 호출되면 수퍼 클래스의 생성자가 호출된다.

 

2. 슈퍼클래스의 인스턴스 생성과 this 바인딩

 

슈퍼클래스의 내부 슬롯인 [[ConstructorKind]]는 "base"로 암묵적인 빈 객체를 생성해 this에 바인딩 한다. 

이때 this가 생성된 인스턴스를 가리킨다.  이 인스턴스는 수퍼 클래스가 생성한 것이다. 하지만 new 연산자와 호출된 클래스는 바로 서브클래스이다. 

즉 new 연산자와 함께 호출된 함수를 가리키는 new.target은 서브클래스를 가리킨다. 따라서 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리된다. 

 

따라서 생성된 인스턴스의 프로토타입은 new.target, 즉 서브 클래스의 prototype 프로퍼티가 가리키는 객체이다.

3. 수퍼클래스의 인스턴스 초기화

 

수퍼클래스의 constructor가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. 즉 this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인자로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.

 

4. 서브클래스 constructor로 복귀와 this 바인딩

 

super의 호출이 종료되고 제어 흐름이 서브클래스 constructor로 돌아온다. 이때 super가 반환한 인스턴스가 this에 바인딩된다.

( super에서 작업이 끝난 인스턴스를 받아서 이어 작업하는 것 같다. 생성자 함수 또한 생성자에서 암묵적으로 this를 반환하고, super도 this를 반환하는데 코드의 실행 흐름이 남아있어 연달아 진행할 수 있다는 것 같다.)

 

서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다.

이 때문에 super가 호출되지 않으면 인스턴스가 생성되지 않으며, this 바인딩도 할 수 없다. 서브 클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없는 이유이다.

 

5. 서브클래스의 인스턴스 초기화

 

super로부터 this에 바인딩되어 있는 인스턴스를 반환받았으면 서브클래스의 constructor의 나머지 코드를 실행한다. 인스턴스의 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.

 

6. 인스턴스 반환

 

클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.

 

반응형

'JavaScript' 카테고리의 다른 글

Rest 파라미터  (0) 2022.03.07
ES6 함수  (0) 2022.03.05
Property Attribute  (0) 2022.03.03
클래스 (1)  (0) 2022.03.02
-집가서 확인  (0) 2022.03.02

댓글