네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

5. Class field declarations proposal

- Field declarations

- private field

- static public fields

 

class Foo {
    x = 1;				// Field declaration 
    #p = 0; 			// Private field
    static y = 20;		// Static public field
    static #sp = 30;	// Static private field
    
    bar() {
    	this.#p = 10; 	// private 필드 참조
        // this.p = 10;	// 새로운 public p 필드를 동적 추가한다.
        return this.#p;
    }
}

const foo = new Foo();
console.log(foo); // Foo { #p: 10, x: 10 }

console.log(foo.x); // 1
// console.log(foo.#p); // SyntaxError: Undefined private field #p: must be declared in an enclosing class
console.log(Foo.y); // 20
// console.log(Foo.#sp); // SyntaxError: Undefined private field #sp: must be declared in an enclosing class
console.log(foo.bar()); // 10

위 예제는 최신 브라우저(Chrome 72 이상) 또는 최신 Node.js(버전 12 이상)에서 정상 동작한다.

 

6. getter, setter

1) getter

getted는 클래스 필드에 접근할 때마다 클래스 필드의 값을 조작하는 행위가 필요할 때 사용한다. getter는 메서드 이름 앞에 get 키워드를 사용해 정의한다. 이 때 메서드 이름은 클래스 필드 이름처럼 사용된다. 다시 말해 getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식으로 사용하며 참조 시에 메서드가 호출된다. getter는 이름 그대로 무언가를 취득할 때 사용하므로 반드시 무언가를 반환해야 한다.

class Foo {
    constructor(arr = []) {
    	this._arr = arr
    }
    
    // getter: get 키워드 뒤에 오는 메서드 이름 firstElem은 클래스 필드 이름처럼 사용된다.
    get FirstElem() {
    	// getter는 반드시 무언가를 반환해야 한다.
        return this._arr.length ? this._arr[0] : null;
    }
}

const foo = new Foo([1, 2]);
console.log(foo.firstElem); // 1

2) setter

setter는 클래스 필드에 값을 할당할 때마다 클래스 필드의 값을 조작하는 행위가 필요할 때 사용한다. setter는 메서드 이름 앞에 set 키워드를 사용해 정의한다. 이때 메서드 이름은 클래스 필드 이름처럼 사용된다. 다시 말해 setter는 호출하는 것이 아니라 프로퍼티처럼 값을 할당하는 형식으로 사용하며 할당 시에 메서드가 호출된다. 사용 방법은 아래와 같다.

class Foo {
    constructor(arr = []) {
    	this._arr = arr;
    }
    
    // getter: get 키워드 뒤에 오는 메서드 이름 firstElem은 클래스 필드 이름처럼 사용된다.
    get firstElem() {
    	// getter는 반드시 무언가를 반환해야 한다.
        return this._arr.length ? this._arr[0] : null;
    }
    
    set firstElem(elem) {
    	// ... this._arr은 this._arr를 개별요소로 분리한다.
        this._arr = [elem, ...this._arr];
    }
}

const foo = new Foo([1, 2]);

// 클래스 필드 firstElem에 값을 할당하면 setter가 호출된다.
foo.firstElem = 100;

console.log(foo.firstElem); // 100

getter와 setter는 클래스에서 새롭게 도입된 기능이 아니다. getter와 setter는 접근자 프로퍼티(accessor property)이다.

// _arr은 데이터 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(foo, '_arr'));
// {value: Array(2), writable: true, enumerable: true, configurable: true}

// firstElem은 접근자 프로퍼티이다. 접근자 프로퍼티는 프로토타입의 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(Foo.prototype, 'firstElem'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

7. 정적 메서드

클래스의 정적(static) 메서드를 정의할 때 static 키워드를 사용한다. 정적 메서드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.

class Foo {
    constructor(prop) {
    	this.prop = prop;
    }
    
    static staticMethod() {
    	/*
       	정적 메서드는 this를 사용할 수 없다.
        정적 메서드는 내부에서 this는 클래스의 인스턴스가 아닌 클래스 자신을 가리킨다.
        */
        return 'staticMethod';
    }
    
    prototypeMethod() {
    	return this.prop;
    }
}

// 정적 메서드는 클래스 이름으로 호출한다.
console.log(Foo.staticMethod());

const foo = new Foo(123); 
// 정적 메서드는 인스턴스로 호출할 수 없다.
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

클래스의 정적 메서드는 인스턴스로 호출할 수 없다. 이것은 정적 메서드는 this를 사용할 수 없다는 것을 의미한다. 일반 메서드 내부에서 this는 클래스의 인스턴스를 가리키며, 메서드 내부에서 this를 사용한다는 것은 클래스의 인스턴스의 생성을 전제로 하는 것이다.

정적 메서드는 클래스 이름으로 호출하기 때문에 클래스의 인스턴스를 생성하지 않아도 사용할 수 있다. 단, 정적 메서드는 this를 사용할 수 없다. 달리 말하면 메서드 내부에서 this를 사용할 필요가 없는 메서드는 정적 메서드로 만들 수 있다. 정적 메서드는 Math 객체의 메서드처럼 애플리케이션 전역에서 사용할 유틸리티(Utility) 함수를 생성할 때 주로 사용한다.

정적 메서드는 클래스의 인스턴스 생성없이 클래스의 이름으로 호출하며 클래스의 인스턴스로 호출할 수 없다고 하였다. 

사실 클래스도 함수고 기존 prototype 기반 패턴의 Syntactic sugar일 뿐이다.

class Foo {}

console.log(typeof Foo); // function

위 코드를 ES5로 표현해보면 아래와 같다. ES5로 표현한 아래의 코드는 ES6의 클래스로 표현한 코드와 정확히 동일하게 동작한다.

var Foo = (function () {

    // 생성자 함수
  	function Foo(prop) {
    	this.prop = prop;
    }
    
    Foo.staticMethod = function() {
    	return 'staticMethod';
    };
    
    Foo.prototype.prototypeMethod = function() {
    	return this.prop;
    }
    
    return Foo;
}());

var foo = new Foo(123);
console.log(foo.prototypeMethod()); // 123
console.log(Foo.staticMethod()); // staticMethod
console.log(foo.staticMethod()); // Uncaught TypeError: foo.staticMethod is not a function

함수 객체(자바스크립트의 함수는 객체이다.) 는 prototype 프로퍼티를 갖는데 일반 객체와는 다른 것이며 일반 객체는 prototype 프로퍼티를 가지지 않는다.

함수 객체만이 가지고 있는 prototype 프로퍼티는 함수 객체가 생성자로 사용될 때, 이 함수를 통해 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다. 위 코드에서 Foo는 생성자 함수로 사용되므로 생성자 함수 Foo의 prototype 프로퍼티가 가리키는 프로토타입 객체는 생성자 함수 Foo를 통해 생성되는 인스턴스 foo의 부모 역할을 한다.

console.log(Foo.prototype === foo.__proto__); // true

그리고 생성자 함수 Foo의 prototype 프로퍼티가 가리키는 프로토타입 객체가 가지고 있는 constructor 프로퍼티는 생성자 함수 Foo를 가리킨다.

console.log(Foo.prototype.constructor === Foo); // true

정적 메서드인 staticMethod는 생성자 함수 Foo의 메서드(함수는 객체이므로 메서드를 가질 수 있다.)이고, 일반 메서드인 prototypeMethod는 프로토타입 객체 Foo.prototype의 메서드이다. 따라서 staticMethod는 foo에서 호출할 수 없다.

 

프로토타입과 정적 메서드

8. 클래스 상속

클래스 상속(Class Inheritance)은 코드 재사용 관점에서 매우 유용하다. 새롭게 정의할 클래스가 기존에 있는 클래스와 매우 유사하다면, 상속을 통해 그대로 사용하되 다른 점만 구현하면 된다.

1) extends 키워드

extends 키워드는 부모 클래스(base class)를 상속받는 자식 클래스(sub class)를 정의할 때 사용한다. 부모 클래스 Circle을 상속받는 자식 클래스 Cylinder를 정의하자.

// 부모 클래스
class Circle {
    constructor(radius) {
    	this.radius = radius;
    }
    
    getDiameter() {
    	return 2 * this.radius;
    }
    
    getPerimeter() {
    	return 2 * Math.PI * this.radius;
    }
    
    getArea() {
    	return Math.PI * Math.pow(this.radius, 2);
    }
}

// 자식 클래스
class Cylinder extends Circle {
    constructor(radius, height) {
    	super(radius);
        this.height = height;
    }
    
    getArea() {
    	return (this.height * super.getPerimeter()) + (2 * super.getArea());
    }
    
    getVolume() {
    	return super.getArea() * this.height;
    }
}

const cylonder = new Cylinder(2, 10);

// 원의 지름
console.log(cylinder.getDiameter());  // 4
// 원의 둘레
console.log(cylinder.getPerimeter()); // 12.566370614359172
// 원통의 넓이
console.log(cylinder.getArea());      // 150.79644737231007
// 원통의 부피
console.log(cylinder.getVolume());    // 125.66370614359172

// cylinder는 Cylinder 클래스의 인스턴스이다.
console.log(cylinder instanceof Cylinder); // true
// cylinder는 Circle 클래스의 인스턴스이다.
console.log(cylinder instanceof Circle);   // true

* 오버라이딩(Overriding)

상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식

* 오버로딩(Overloading)

매개변수의 타입 또는 갯수가 다른, 같은 이름의 메서드를 구현하고 매개변수에 의해 메서드를 구별하여 호출하는 방식이다. 자바스크립트는 오버로딩을 지원하지 않지만 arguments 객체를 사용하여 구현할 수 는 있다.

 

위 코드를 프로토타입 관점으로 표현하면 아래와 같다. 인스턴스 cylinder는 프로토타입 체인에 의해 부모 클래스 Circle의 메서드를 사용할 수 있다.

클래스 상속

console.log(cylinder.__proto__ === Cylinder.prototype); // true
console.log(Cylinder.prototype.__proto__ === Circle.prototype); // true
console.log(Circle.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

프로토타입 체인은 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때 프로퍼티 또는 메서드가 없다면 [[Prototype]] 내부 슬롯이 가리키는 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메서드를 차례대로 검색한다. 그리고 검색에 성공하면 그 프로퍼티나 메서드를 사용한다.

2) super 키워드

super 키워드는 부모 클래스를 참조(Reference)할 때 또는 부모 클래스의 constructor를 호출할 때 사용한다.

 

3) static 매서드와 prototype 메서드의 상속

프로토타입 관점에서 바라보면 자식 클래스의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입 객체는 부모 클래스이다.

class Parent {}
class Child extends Parent {}

console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

자식 클래스의 Child의 프로토타입 객체는 부모 클래스의 Parent이다.

이것을 프로토타입 체인(Prototype Chain)에 의해 부모 클래스의 정적 메서드도 상속됨을 의미한다.

class Parent {
    static staticMethod() {
    	return 'staticMethod';
    }
}

class Child extends Parent {}

console.log(Parent.staticMethod()); // 'staticMethod'
console.log(Child.staticMethod()); // 'staticMethod'

자식 클래스의 정적 메서드 내부에서도 super 키워드를 사용하여 부모 클래스의 정적 메서드를 호출할 수 있다. 자식 클래스는 프로토타입 체인에 의해 부모 클래스의 정적 메서드를 참조할 수 있기 때문이다.

하지만 자식 클래스의 일반 메서드(프로토타입 메서드) 내부에서는 super 키워드를 사용하여 부모 클래스의 정적 메서드를 호출할 수 없다. 이는 자식 클래스의 인스턴스는 프로토타입 체인에 의해 부모 클래스의 정적 메서드를 참조할 수 없기 때문이다.

class Parent {
    static staticMethod() {
    	return 'Hello';
    }
}

class Child extends Parent {
	static staticMethod() {
    	return `${super.staticMethod()} world`;
    }
    
    prototypeMethod() {
    	return `${super.staticMethod()} world`;
    }
}

console.log(Parent.staticMethod()); // 'Hello'
console.log(Child.staticMethod()); // 'Hello world'
console.log(Child.prototypeMethod()); // TypeError: {intermediate value).staticMethod is not a function

prototype chain에 의한 메서드 상속

728x90
반응형

'programming language > ECMAScript6' 카테고리의 다른 글

[ES6] 프로미스 (Promise)  (1) 2021.12.27
[ES6] 모듈 (Module)  (0) 2021.12.20
[ES6] 클래스(1)  (2) 2021.12.15
[ES6] 디스트럭처링 (Destructuring)  (0) 2021.12.14
[ES6] 객체 리터럴 프로퍼티 기능 확장  (0) 2021.12.13
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !