https://infoscis.github.io/2018/02/13/ecmascript-6-introducing-javascript-classes/
JavaScript 클래스 소개
대부분의 공식적인 객체 지향 프로그래밍 언어와는 달리 JavaScript는 만들어질 때 부터 유사 객체 및 관련 객체를 정의하는 주요 방법으로 클래스와 클래스 상속을 지원하지 않았습니다. 이로 인해 많은 개발자들이 혼란 스러웠고, ECMAScript 1 이전 버전부터 ECMAScript 5 까지 많은 라이브러리가 클래스를 지원하는 것처럼 보이게하는 유틸리티를 만들었습니다. 일부 JavaScript 개발자는 JavaScript에 클래스는 필요하지 않다고 강력하게 느낄수도 있지만, 클래스 지원을 목적으로 하는 많은 수의 라이브러리는 클래스를 ECMAScript 6에 포함 시키도록 했습니다.
클래스가 사용하는 기본 메커니즘을 이해하는 것이 ECMAScript 6 클래스를 공부하는데 도움이되므로 이 장에서는 ECMAScript 5 개발자가 클래스와 비슷한 동작을 하는 방법에 대해 논의함으로써 시작합니다. 그러나 나중에 보게 되겠지만, ECMAScript 6 클래스는 다른 언어의 클래스와 완전히 동일하지 않습니다. JavaScript의 동적 특성을 반영하는 독창적인 기능이 있습니다.
ECMAScript 5의 클래스와 비슷한 구조
ECMAScript 5 이전 버전의 JavaScript에는 클래스가 없었습니다. 클래스에 가장 가까운 방법은 생성자를 생성한 다음 생성자의 프로토 타입에 메서드를 할당하는 것으로, 일반적으로 사용자 정의 타입 생성이라고 하는 접근 방식입니다.
|
|
이 코드에서 PersonType
은 name
이라는 단일 프로퍼티를 생성하는 생성자 함수입니다. sayName()
메서드는 프로토 타입에 할당되어 동일한 함수가 PersonType
객체의 모든 인스턴스에 의해 공유됩니다. 그런 다음, Person
의 새로운 인스턴스가 new
연산자를 통해 생성됩니다. person
객체는 프로토 타입 상속을 통해 PersonType
과 Object
인스턴스로 간주됩니다.
이 기본 패턴은 클래스를 모방하는 많은 JavaScript 라이브러리의 근간을 이루며 ECMAScript 6 클래스가 시작됩니다.
클래스 선언
ECMAScript 6에서 가장 간단한 클래스 형식은 다른 언어의 클래스와 비슷한 클래스 선언입니다.
클래스 선언의 기본
클래스 선언은 class
키워드와 클래스의 이름으로 시작됩니다. 구문의 나머지 부분은 객체 리터럴의 간결한 메서드와 비슷하지만 쉼표가 필요하지 않습니다. 예를 들어, 다음은 간단한 클래스 선언입니다.
|
|
PersonClass
클래스 선언은 이전 예제의 PersonType
과 매우 유사하게 동작합니다. 그러나 함수를 생성자로 정의하는 대신 클래스 선언을 사용하면 특수한 constructor
메서드 이름을 사용하여 클래스 내부에 직접 생성자를 정의할 수 있습니다. 클래스 메서드는 간결한 구문을 사용하기 때문에 function
키워드를 사용할 필요가 없습니다. 그리고 다른 메서드 이름에는 특별한 의미가 없으므로 원하는 만큼 메서드를 추가할 수 있습니다.
Own property는 프로토 타입이 아닌 인스턴스에서 보여지는 프로퍼티는 클래스 생성자 또는 메서드 내부에서만 만들 수 있습니다. 이 예제에서
name
은 Own Property입니다. 생성자 함수 내에서 가능한 모든 프로퍼티를 만드는 것이 좋습니다. 왜냐하면 클래스의 한 장소에서 모든 프로퍼티가 표현되기 때문입니다.
흥미롭게도, 클래스 선언은 기존 사용자 정의 타입 선언의 Syntactic sugar입니다. PersonClass
선언은 실제로 constructor
메서드의 동작을 갖는 함수를 생성합니다. 이것이 typeof PersonClass
가 “function
“을 결과로 주는 이유입니다. sayName()
메서드는 앞의 예제에서 sayName()
과 PersonType.prototype
사이의 관계와 비슷하게 이 예제에서 PersonClass.prototype
에 대한 메서드로 끝납니다. 이러한 유사점을 사용하면 사용자가 사용하는 타입에 대해 너무 많이 걱정하지 않고도 사용자 정의 타입 및 클래스를 혼합할 수 있습니다.
클래스 구문을 사용해야하는 이유
클래스와 사용자 정의 타입의 유사점에도 불구하고 유의해야 할 몇 가지 중요한 차이점이 있습니다.
- 클래스 선언은 함수 선언과 달리 Hoisting되지 않습니다. 클래스 선언은
let
선언과 같이 행동하며, 실행이 선언에 도달할 때까지 Temporal dead zone에 존재합니다. - 클래스 선언의 모든 코드는
strict
모드로 자동 실행됩니다. 클래스의strict
모드를 거부할 수있는 방법이 없습니다. - 모든 메서드는 Non-enumerable 입니다.
Object.defineProperty()
를 사용하여 메서드를 Non-enumerable하게 만드는 사용자 지정 타입과 다른 중요한 변경 사항입니다. - 모든 메서드는 내부
[[Construct]]
메서드가 없으며new
로 호출하려고 하면 에러가 발생합니다. new
를 사용하지 않고 클래스 생성자를 호출하면 오류가 발생합니다.- 클래스 메서드 내에서 클래스 이름을 덮어 쓰려고하면 오류가 발생합니다.
이 모든 것을 염두에 두고, 위 예제의 PersonClass
선언은 클래스 구문을 사용하지 않는 다음 코드와 동일합니다.
|
|
먼저 두 개의 PersonType2
선언이 있음을 주목하십시오(외부 scope에 있는 let
선언과 IIFE 내부에 있는 const
선언). 이것은 클래스 메서드가 클래스 이름을 덮어 쓰는 것을 금지하는 반면 클래스 외부의 코드는 이를 허용합니다. 생성자 함수는 new.target
을 검사하여new
로 호출되는지 확인합니다. 그렇지 않은 경우 오류가 발생합니다. 다음으로, sayName()
메소드는 Non-enumerable으로 정의되고 메t서드는 new.target
을 검사하여 new
로 호출되지 않았음을 확인합니다. 마지막 단계는 생성자 함수를 반환합니다.
이 예제는 새로운 구문을 사용하지 않고 클래스가 수행할 수있는 모든 작업할 수 있지만 클래스 구문은 모든 기능을 크게 단순화한다는 것을 보여줍니다.
상수 클래스 이름
클래스의 이름은 클래스 내부에서 const
를 사용하는 경우에만 지정됩니다. 즉, 클래스 외부의 클래스 이름은 덮어 쓸 수 있지만 클래스 메서드 내부에서는 덮어 쓸 수 없습니다.
|
|
이 코드에서 클래스 생성자 안에있는 Foo
는 클래스 외부의 Foo
와는 별도의 바인딩입니다. 내부의 Foo
는 마치 const
인 것처럼 정의되고 덮어 쓸 수 없습니다. 생성자가 Foo
를 임의의 값으로 덮어 쓰려고하면 에러가 발생합니다. 그러나 외부 Foo
는 let
선언처럼 정의되기 때문에 언제든지 값을 덮어 쓸 수 있습니다.
클래스 표현식
클래스와 함수는 선언과 표현식이라는 두가지 형식을 가지고 있다는 점에서 비슷합니다. 함수와 클래스 선언은 적절한 키워드 (각각function
또는class
)와 식별자로 시작됩니다. 함수는 function
다음에 식별자를 필요로하지 않는 표현식 형태를 가지고 있고, 비슷하게 클래스는 class
다음에 식별자를 필요로하지 않는 표현식 형태를 가지고 있습니다. 이 클래스 표현식은 변수 선언에 사용되거나 함수로 인수로 전달되도록 설계되었습니다.
기본 클래스 표현식
다음은 이전 PersonClass
예제에 해당하는 클래스 표현식과 그 코드를 사용하는 코드입니다.
|
|
이 예제에서 알 수 있듯이, 클래스 표현식은 class
다음에 식별자를 요구하지 않습니다. 구문 외에도 클래스 표현식은 클래스 선언과 기능적으로 동일합니다.
클래스 선언 또는 클래스 표현식 사용 여부는 주로 스타일의 문제입니다. 함수 선언 및 함수 표현식과 달리 클래스 선언과 클래스 표현식은 모두 Hoisting되지 않기 때문에 코드의 런타임 동작에 거의 영향을 미치지 않습니다.
이름이 부여된 클래스 표현식
이전 섹션 예제에서 익명 클래스 표현식을 사용했지만 함수 표현식과 마찬가지로 클래스 표현식의 이름을 지정할 수도 있습니다. 이렇게 하려면 다음과 같이 class
키워드 다음에 이름(식별자)를 포함 시킵니다.
|
|
이 예제에서 클래스 표현식의 이름은 PersonClass2
입니다. PersonClass2
식별자는 클래스 정의 내에서만 존재하기 때문에 (이 예제에서sayName()
메서드 처럼) 클래스 메서드 내부에서 사용될 수 있습니다. 하지만 클래스 밖에서 typeof PersonClass2
는 PersonClass2
바인딩이 존재하지 않기 때문에 "undefined"
입니다. 이러한 이유를 이해하기 위해 아래 예제처럼 클래스를 사용하지 않는 동일한 함수 선언을 살펴보겠습니다.
|
|
이름이 부여된 클래스 표현식을 작성하면 JavaScript 엔진에서 일어나는 일이 약간 바뀝니다. 클래스 선언의 경우, 외부 바인딩 (let
으로 정의)은 내부 바인딩 (const
로 정의)과 동일한 이름을 가집니다. 이름이 부여된 클래스 표현식은 const
정의에서 그 이름을 사용하므로PersonClass2
는 클래스 내부에서만 사용하도록 정의됩니다.
이름이 부여된 클래스 표현식은 이름이 부여된 함수 표현식과 다르게 동작하지만, 두 표현식 사이에는 여전히 많은 유사점이 있습니다. 둘 다 값(Value)으로 사용할 수 있으며, 이는 많은 가능성을 열어줍니다. 이것은 아래에서 다루도록 하겠습니다.
일급 시민(First-Class Citizen)으로서의 클래스
일급 시민: 컴퓨터 프로그래밍 언어 디자인에서, 특정 언어의 일급 객체 (first-class citizens, 일급 값, 일급 엔티티, 혹은 일급 시민)이라 함은 일반적으로 다른 객체들에 적용 가능한 연산을 모두 지원하는 객체를 가리킨다. 함수에 파라미터로 넘기기, 변수에 대입하기와 같은 연산들이 여기서 말하는 일반적인 연산의 예에 해당한다.
프로그래밍 언어에서 어떤것은 값(Value)으로 사용될 수 있고, 이 경우에 First-Class Citizen이라고 말합니다. 즉, 함수로 전달되고 함수에서 반환되며 변수에 할당될 수 있습니다. JavaScript 함수는 First-Class Citizen (때로는 First-Class Function이라 부름)이며, 이는 JavaScript를 고유하게 만드는 요소의 일부입니다.
ECMAScript 6은 클래스를 First-Class Citizen으로 만들어 이러한 전통을 이어가고 있습니다. 이를 통해 클래스를 다양한 방식으로 사용할 수 있습니다. 예를 들어, 파라미터로 함수에 전달할 수 있습니다.
|
|
이 예제에서, createObject()
함수는 익명의 클래스 표현식을 인수로하여 호출되고, new
로 그 클래스의 인스턴스를 생성하여 인스턴스를 리턴합니다. 변수 obj
는 반환된 인스턴스를 저장합니다.
클래스 표현식의 또 다른 흥미로운 사용법은 클래스 생성자를 즉시 호출하여 싱글톤을 생성하는 것입니다. 이렇게 하려면 클래스 표현식에new
를 사용해야 하고 마지막에 괄호를 포함해야합니다.
|
|
여기서 익명의 클래스 표현식이 생성되고 즉시 실행됩니다. 이 패턴을 사용하면 클래스 참조를 검사할 수 있게하지 않고도 싱글톤을 생성하기
위한 클래스 구문을 사용할 수 있습니다(PersonClass
는 바깥 쪽이 아닌 클래스 내에서만 바인딩을 생성한다는 것을 기억하십시오.). 클래스 표현식의 끝 부분에있는 괄호는 함수를 호출하는 동시에 인자를 전달할 수 있는 것을 나타냅니다.
지금까지 이 장의 예제는 메서드가 있는 클래스에 중점을 두었습니다. 그러나 객체 리터럴과 유사한 구문을 사용하여 클래스에 접근자(Accessor) 프로퍼티를 만들 수도 있습니다.
접근자(Accessor) 프로퍼티
클래스 생성자 내에서 자체 프로퍼티를 만들어야 하지만 클래스를 사용하면 프로토 타입에 접근자(Accessor) 프로퍼티를 정의할 수 있습니다. Getter를 만들려면 키워드 get
다음에 공백 문자와 식별자를 사용하합니다. Setter를 만들려면 set
키워드를 사용합니다.
|
|
이 코드에서, CustomHTMLElement
클래스는 기존 DOM Element를 감싸는 래퍼로 만들어집니다. 그것은 Element 자체에 대한 innerHTML
메서드에 위임한 html
을 위한 Getter와 Setter를 둘 다 가지고 있습니다. 이 접근자 프로퍼티는 CustomHTMLElement.prototype
에서 생성되며, 다른 메서드와 마찬가지로 Non-enumerable로 생성됩니다. 클래스를 사용하지 않는 동일한 코드는 아래와 같습니다.
|
|
이전 예제와 마찬가지로 이 코드는 클래스를 이용하는 것이 동일한 기능을 하는 클래스를 사용하지 않는 코드에 비해 얼마나 코드를 줄일수 있는지 보여줍니다. html
접근자 프로퍼티 정의만이 거의 비슷한 크기입니다.
계산된 멤버 이름
객체 리터럴과 클래스 간의 유사점은 아직 끝나지 않았습니다. 클래스 메서드와 접근자 프로퍼티는 계산된 이름을 가질 수도 있습니다. 식별자를 사용하는 대신 표현식 주위에 대괄호를 사용합니다. 이 표현식은 객체 리터럴의 계산된 이름에 사용되는 구문과 동일합니다.
|
|
이 버전의 PersonClass
는 변수를 사용하여 정의 안에 있는 메서드에 이름을 할당합니다. 문자열 "sayName"
은 methodName
변수에 할당되고 메서드를 선언하기 위해 methodName
이 사용됩니다. sayName()
메서드는 나중에 직접 액세스됩니다.
접근자 프로퍼티는 다음과 같은 방식으로 계산된 이름을 사용할 수 있습니다.
|
|
여기서 html
에 대한 Getter와 Setter는 propertyName
변수를 사용하여 설정됩니다. .html
을 사용하여 프로퍼티에 접근하는 것은 정의에만 영향을 줍니다.
클래스와 객체 리터럴 간에는 메서드, 접근자 프로퍼티 및 계산된 이름등 많은 유사점이 있다는 것을 알았습니다. 그리고 Generator라는 유사점이 하나더 있습니다.
Generator 메서드
8 장에서 Generator를 소개할 때 메서드 이름에 별표 (*
)를 추가하여 객체 리터럴에 Generator를 정의하는 방법을 배웠습니다. 클래스에 대해서도 동일한 구문이 적용되어 모든 메서드를 Generator로 사용할 수 있습니다. 다음은 그 예입니다.
|
|
이 코드는 createIterator()
Generator 메서드를 가지는 MyClass
라는 클래스를 생성합니다. 이 메서드는 값이 Generator에 하드코딩된 Iterator를 반환합니다. Generator 메서드는 값의 컬렉션을 나타내는 객체가 있고 해당 값을 쉽게 반복할 때 유용합니다. Array, Set, Map은 모두 개발자들이 아이템과 상호 작용할 필요가 있는 다양한 방법을 설명하기 위해 여러 Generator 메서드를 가지고 있습니다.
클래스에 대한 기본 Iterator를 정의하면 클래스가 값 컬렉션을 나타내는 경우 훨씬 유용합니다. Symbol.iterator
를 사용하여 다음과 같이 Generator 메서드를 정의하여 클래스의 기본 Iterator를 정의할 수 있습니다.
|
|
이 예제는 this.items
배열의 values()
Iterator에 위임한 Generator 메서드에 대해 계산된 이름을 사용합니다. 컬렉션의 값을 관리하는 모든 클래스에는 기본 Iterator가 포함되어야 합니다. 컬렉션 관련 일부 작업에는 Iterator가 필요하기 때문입니다. 이제, Collection
의 어떤 인스턴스도 for-of
루프나 Spread 연산자에 직접 사용될 수 있습니다.
클래스 프로토 타입에 메서드 및 접근자 프로퍼티를 추가하면 객체 인스턴스에 해당 메서드를 표시할 때 유용합니다. 반면에 클래스 자체의 메서드 또는 접근자 프로퍼티를 원하면 정적 멤버를 사용해야합니다.
정적 멤버(Static Member)
정적 멤버를 시뮬레이트하기 위해 생성자에 직접 메서드를 추가하는 것은 ECMAScript 5 및 이전 버전의 또 다른 공통 패턴입니다.
|
|
다른 프로그래밍 언어에서, PersonType.create()
라고 불리는 팩토리 메소드는 정적 메소드로 간주될 것입니다. 왜냐하면 PersonType
의
인스턴스에 의존하지 않기 때문입니다. ECMAScript 6 클래스는 메서드 또는 접근자 프로퍼티 이름 앞에 정식 static
Annotation을 사용하여 정적 멤버 생성을 단순화합니다. 예를 들어, 다음은 이전 예제와 동일한 클래스입니다.
|
|
PersonClass
정의는 create()
라고 하는 하나의 정적 메서드를 가지고 있습니다. 메서드 구문은 static
키워드를 제외하고 sayName()
과 동일합니다. static
키워드는 클래스 내의 임의의 메서드 또는 접근자 프로퍼티 정의에 사용할 수 있습니다. 유일한 제약은static
을 constructor
메서드 정의와 함께 사용할 수 없다는 것입니다.
정적 멤버는 인스턴스에서 액세스할 수 없습니다. 항상 클래스의 정적 멤버에 직접 액세스해야합니다.
파생 클래스를 사용한 상속
ECMAScript 6 이전에는 사용자 정의 타입으로 상속을 구현할때 대규모 과정이 필요했습니다. 적절한 상속에는 여러 단계가 필요했습니다. 예를 들어 다음 예제를 살펴보겠습니다.
|
|
Square
는 Rectangle
을 상속받습니다. 그래서 Square.prototype
을 Rectangle.prototype
에서 생성한 새로운 객체로 덮어 쓰고Rectangle.call()
메서드를 호출해야합니다. 이 단계들은 종종 JavaScript 신참을 혼란스럽게 만들거나 경험 많은 개발자에게 오류의 원인이 되었습니다.
클래스는 익숙한 extends
키워드를 사용하여 클래스가 상속해야하는 함수를 지정함으로써 상속을 보다 쉽게 구현할 수 있도록합니다. 프로토 타입은 자동으로 조정되며 super()
메서드를 호출하여 기본 클래스 생성자에 액세스할 수 있습니다. 다음은 앞의 예제와 동일한 ECMAScript 6 코드입니다.
|
|
이번에 Square
클래스는 extends
키워드를 사용하여 Rectangle
에서 상속받습니다. Square
생성자는 super()
를 사용하여 지정된 파라미터로 Rectangle
생성자를 호출합니다. ECMAScript 5 버전의 코드와 달리 식별자 Rectangle
은 클래스 선언 (extends
이후)에서만 사용됩니다.
다른 클래스로부터 상속받은 클래스를 파생 클래스(derived classes)라고합니다. 파생 클래스는 생성자를 지정하는 경우 super()
를 사용해야합니다. 그렇지 않으면 오류가 발생합니다. 생성자를 사용하지 않기로 결정한 경우 클래스의 새 인스턴스를 만들 때 super()
가 모든 파라미터와 함께 자동으로 호출됩니다. 예를 들어, 다음 두 클래스는 동일합니다.
|
|
이 예제의 두 번째 클래스는 모든 파생 클래스에 대한 기본 생성자와 동일합니다. 모든 파라미터는 순서대로 기본 클래스 생성자에 전달됩니다. 이전에 정의한 예제에서 Square
클래스의 생성자는 하나의 파라미터만 필요하기 때문에 super(...args)
는 올바르지 않으므로 수동으로 생성자를 정의하는 것이 좋습니다.
super()
를 사용할 때 유의해야할 몇 가지 사항이 있습니다.
파생 클래스에서만
super()
를 사용할 수 있습니다. 비 파생 클래스 (extends
를 사용하지 않는 클래스) 또는 함수에서 사용하려고
하면 오류가 발생합니다.생성자에서
this
에 접근하기 전에super()
를 호출해야합니다.super()
는this
를 초기화할 책임이 있기 때문에super()
를 호출하기 전에this
에 접근하려고 하면 에러가 발생합니다.
super()
를 호출하지 않는 유일한 방법은 클래스 생성자에서 객체를 반환하는 것입니다.
클래스 메서드 숨기기
파생 클래스의 메서드는 항상 기본 클래스에서 같은 이름의 메서드를 숨깁니다. 예를 들어, Square
에 getArea()
를 추가하면 그 기능을 재정의할 수 있습니다.
|
|
getArea()
가 Square
의 일부로 정의 되었기 때문에 Rectangle.prototype.getArea()
메서드는 Square
인스턴스에 의해 더이상 호출되지 않습니다. 물론, 다음과 같이 super.getArea()
메서드를 사용하여 메서드의 기본 클래스 버전을 호출하기로 결정할 수 있습니다.
|
|
이런 방식으로 super
를 사용하는 것은 4 장에서 논의된 super
참조와 똑같이 작동합니다 (“Super 참조를 사용한 쉬운 프로토 타입 액세스” 참조). this
값은 자동으로 올바르게 설정되어 간단하게 메소드 호출할 수 있습니다.
정적 멤버 상속
기본 클래스에 정적 멤버가 있는 경우 해당 정적 멤버는 파생 클래스에서도 사용할 수 있습니다. 상속은 다른 언어의 경우와 마찬가지로 작동하지만 이는 JavaScript에서는 새로운 개념입니다. 다음은 그 예입니다.
|
|
이 코드에서는 새로운 정적 create()
메서드가 Rectangle
클래스에 추가되었습니다. 이 메서드는 상속을 통해 Square.create()
로 사용할 수 있으며 Rectangle.create()
메서드와 같은 방식으로 동작합니다.
표현식에서 파생된 클래스
아마도 ECMAScript 6에서 파생 클래스의 가장 강력한 부분은 표현식에서 클래스를 파생시킬 수 있는 능력입니다. 표현식이 [[Construct]]
와 프로토 타입을 가진 함수로 해석되는한 어떤 표현식이라도 extends
을 사용할 수 있습니다.
|
|
Rectangle
은 ECMAScript 5 스타일 생성자로 정의되고 Square
는 클래스입니다. Rectangle
은 [[Construct]]
와 프로토 타입을 가지고 있기 때문에 Square
클래스는 직접 상속받을 수 있습니다.
extends
이후에 어떤 타입의 표현식이라도 받아들이면 상속받을 것을 동적으로 결정하는 것과 같은 강력한 가능성을 제공합니다.
|
|
getBase()
함수는 클래스 선언의 일부로서 직접 호출되어 Rectangle
을 반환합니다. 이 예제는 기능적으로 이전의 것과 같습니다. 그리고 기본 클래스를 동적으로 결정할 수 있으므로 서로 다른 상속 접근법을 만들 수 있습니다. 예를 들어 mixin을 효과적으로 만들 수 있습니다.
|
|
이 예에서는 클래식 상속 대신 mixin이 사용됩니다. mixin()
함수는 mixin 객체를 나타내는 파라미터를 취합니다. base
라는 함수를 생성하고 각 mixin 객체의 프로퍼티를 프로토 타입에 할당합니다. mixin()
함수는 Square
가 extends
를 사용할 수 있도록 함수를 반환합니다. extends
가 여전히 사용되기 때문에 생성자에서 super()
를 호출해야 한다는 것을 명심하십시오.
Square
의 인스턴스는 AreaMixin
의 getArea()
와 SerializableMixin
의 serialize()
를 가지고 있습니다. 이는 프로토 타입 상속을 통해 수행됩니다. mixin()
함수는 새로운 함수의 프로토 타입을 각 mixin
의 모든 프로퍼티로 동적으로 채웁니다. (여러 mixin이 동일한 속성을 갖는 경우 마지막 프로퍼티만 추가됩니다.)
모든 표현식은
extends
다음에 사용될 수 있지만, 모든 표현식이 유효한 클래스가되는 것은 아닙니다. 특히 다음 표현식 유형은 오류를 유발합니다.
null
- Generator 함수 (Chapter 8에서 설명함)
이러한 경우, 호출할
[[Construct]]
가 없으므로 클래스의 새 인스턴스를 만들려고하면 오류가 발생합니다.
내장(Built-in) 상속
JavaScript Array가 추가될때부터 개발자는 상속을 통해 자신만의 특별한 Array 타입을 만들고 싶어했습니다. ECMAScript 5 및 이전 버전에서는 이것이 가능하지 않았습니다. 고전적인 상속을 사용하려고 시도해도 코드가 작동하지 않았습니다.
|
|
이 코드의 끝 부분에 있는 console.log()
출력은 Array에 JavaScript의 고전적인 상속 형태를 사용하여, 어떤 예기치 않은 동작을 일으키는 방법을 보여줍니다. MyArray
의 인스턴스에서 length
와 숫자 프로퍼티는 Array.apply()
또는 prototype을 할당해도 Built-in Array에서 처럼 동작하지 않습니다.
ECMAScript 6 클래스의 목표 중 하나는 모든 Built-in 함수에서 상속을 허용하는 것입니다. 이를 달성하기 위해 클래스의 상속 모델은 ECMAScript 5 및 이전 버전에서 보여준 고전적인 상속 모델과 약간 다릅니다.
ECMAScript 5 전통적인 상속에서 this
의 값은 파생된 타입 (예를 들면, MyArray
)에 의해 먼저 생성되고, Array.apply()
메서드와 같은 기본 타입 생성자가 호출됩니다. 즉, 이것은 MyArray
인스턴스로 시작하여 Array의 추가 프로퍼티로 꾸며져 있음을 의미합니다.
ECMAScript 6 클래스 기반 상속에서, this
의 값은 먼저 기본타입(Array
)에 의해 생성된 다음 파생 클래스 생성자 (MyArray
)에 의해 수정됩니다. 결과적으로 this
는 기본타입의 모든 Built-in 기능으로 시작하여 그에 관련된 모든 기능을 올바르게 상속합니다.
다음 예제는 클래스 기반 특수 Array의 실행을 보여줍니다.
|
|
MyArray
는 Array
에서 직접 상속되므로 Array
와 같이 작동합니다. 숫자 프로퍼티와 상호 작용하면 length
프로퍼티를 업데이트하고, length
프로퍼티를 조작하면 숫자 프로퍼티를 업데이트합니다. 즉, Array
를 상속 받아서 자신만의 파생 Array 클래스를 만들고 다른 Built-in 함수를 상속받을 수 있다는 것을 의미합니다.
Symbol.species 프로퍼티
Built-in 함수의 상속이 흥미로운 점은 Built-in 함수의 인스턴스를 반환하는 모든 메서드가 파생 클래스 인스턴스를 자동으로 반환한다는 것입니다. 그래서 Array
를 상속받은 MyArray
라는 파생 클래스가 있다면 slice()
와 같은 메서드는 MyArray
의 인스턴스를 리턴합니다.
|
|
이 코드에서 slice()
메서드는 MyArray
인스턴스를 반환합니다. slice()
메서드는 Array
로부터 상속 받고 Array
의 인스턴스를 정상적으로 리턴합니다. 하지만 뒤에서 이 변화를 일으키는 것은 Symbol.species
프로퍼티입니다.
Symbol.species
Symbol은 함수를 반환하는 정적(Static) 접근자(Accessor) 프로퍼티를 정의하는데 사용됩니다. 이 함수는 클래스의 인스턴스가 인스턴스 메서드 내부에서 만들어져야 할 때마다 생성자 대신 사용하는 생성자입니다. 아래의 Built-in 타입은 Symbol.species
가 정의되어 있습니다.
- Array
- ArrayBuffer (Chapter 10에서 논의 합니다.)
- Map
- Promise
- RegExp
- Set
- Typed Array (Chapter 10에서 논의 합니다.)
이 각각의 타입은 this
를 반환하는 디폴트 Symbol.species
프로퍼티를 가지고 있습니다. 이것은 프로퍼티가 항상 생성자 함수를 반환한다는 것을 의미합니다. 커스텀 클래스에서 이 기능을 구현한다면 코드는 다음과 같이 보일 것입니다.
|
|
이 예제에서 잘 알려진 Symbol.species
Symbol은 MyClass
에 정적 접근자 프로퍼티를 할당하는데 사용됩니다. 클래스의 타입 변경은 불가능하므로 setter가 없는 getter만 있습니다. this.constructor [Symbol.species]
를 호출하면 MyClass
가 리턴됩니다. clone()
메서드는 직접 MyClass
를 사용하지 않고 클래스 정의(Class Definition)를 사용하여 새로운 인스턴스를 반환합니다. 그리고 파생 클래스가 그 값을 오버라이드(override) 할 수 있습니다.
|
|
여기에서 MyDerivedClass1
은 MyClass
를 상속 받고 Symbol.species
프로퍼티는 변경하지 않습니다. clone()
이 호출되면, this.constructor [Symbol.species]
가 MyDerivedClass1
을 리턴하기 때문에 MyDerivedClass1
의 인스턴스를 리턴합니다. MyDerivedClass2
클래스는 MyClass
를 상속 받아 Symbol.species
를 오버라이드하여 MyClass
를 반환합니다. MyDerivedClass2
의 인스턴스에서 clone()
이 호출될 때, 반환 값은 MyClass
의 인스턴스입니다. Symbol.species
를 사용하여 파생 클래스는 메서드가 인스턴스를 리턴할 때 리턴되어야 하는 값의 타입을 판별할 수 있습니다.
예를 들어 Array
는 Symbol.species
를 사용하여 배열을 반환하는 메서드에 사용할 클래스를 지정합니다. Array
에서 파생된 클래스에서 다음과 같이 상속된 메서드에서 반환된 객체의 타입을 결정할 수 있습니다.
|
|
이 코드는 Array
를 상속받은 MyArray
의 Symbol.species
를 오버라이드합니다. Array를 반환하는 모든 상속된 메서드는 이제MyArray
대신 Array
인스턴스를 사용합니다.
일반적으로 클래스 메서드에서 this.constructor
를 사용하려고 할 때마다 Symbol.species
프로퍼티를 사용해야합니다. 이렇게하면 파생 클래스가 반환 형식을 쉽게 재정의할 수 있습니다. 또한 Symbol.species
가 정의된 클래스에서 파생 클래스를 만드는 경우에도 생성자 대신 사용해야 합니다.
클래스 생성자에서 new.target 사용
3 장에서는 new.target
과 함수가 호출되는 방식에 따라 값이 변경되는 방법에 대해 알게되었습니다. 클래스 생성자에서 new.target
을 사용하여 클래스가 호출되는 방법을 결정할 수도 있습니다. new.target
은 다음 예제와 같이 클래스 생성자 함수와 같습니다.
|
|
이 코드의 new.target
은 new Rectangle(3, 4)
이 호출될 때 Rectangle
과 동일하다는 것을 보여줍니다. 클래스 생성자는 new
가 없으면 호출될 수 없으므로 new.target
프로퍼티는 항상 클래스 생성자 내부에서 정의됩니다. 그러나 그 값이 항상 같지는 않을 수도 있습니다. 다음 코드를 살펴보겠습니다.
|
|
Square
는 Rectangle
생성자를 호출하고 Rectangle
생성자가 호출될 때 new.target
은 Square
와 같습니다. 이는 각 생성자에게 호출되는 방식에 따라 동작을 변경할 수있는 기능을 제공하므로 중요합니다. 예를 들어, 다음과 같이 new.target
을 사용하여 추상적인 기본 클래스 (직접적으로 인스턴스화할 수없는 클래스)를 생성할 수 있습니다.
|
|
이 예제에서 Shape
클래스 생성자는 new.target
이 Shape
일 때마다 에러를 던집니다. 즉, new Shape ()
는 항상 에러를 던집니다. 그러나 여전히 Rectangle
에서 사용하는 것처럼 Shape
을 기본 클래스로 사용할 수 있습니다. super()
호출은 Shape
생성자를 실행하고 new.target
은 Rectangle
과 같습니다. 그래서 생성자는 오류없이 계속됩니다.
new
가 없으면 클래스를 호출할 수 없으므로new.target
프로퍼티는 결코 클래스 생성자 내부에서 정의되지 않습니다.
요약
ECMAScript 6 클래스는 JavaScript에서 상속을 사용하기 쉽게하기 때문에 다른 언어에서 상속에 대한 기존의 이해를 버릴 필요가 없습니다. ECMAScript 6 클래스는 ECMAScript 5의 클래식 상속 모델에 대한 Syntactic sugar로 시작하지만 실수를 줄이기위한 많은 기능을 추가합니다.
ECMAScript 6 클래스는 클래스 프로토 타입에 비 정적 메서드를 정의하여 프로토 타입 상속과 함께 작동하고, 정적 메서드는 생성자 자체에서 끝납니다. 클래스의 모든 메소드는 Non-enumerable이며, 기본적으로 Non-enumerable인 Built-in 객체의 동작과 더 잘 일치하는 기능입니다. 또한 클래스 생성자는 new
가 없으면 호출할 수 없으므로 실수로 클래스를 함수로 호출할 수 없습니다.
클래스 기반 상속을 사용하면 다른 클래스, 함수 또는 표현식에서 클래스를 파생시킬 수 있습니다. 이 기능을 사용하여 상속할 올바른 기준을 결정하는 함수를 호출할 수 있으므로 mixin 및 다른 여러 구성 패턴을 사용하여 새 클래스를 만들 수 있습니다. 상속은 Array
와 같은 Built-in 객체를 상속하는 것이 가능합니다.
클래스 생성자에서 new.target
을 사용하여 클래스가 어떻게 호출되는지에 따라 다르게 행동할 수 있습니다. 가장 보편적인 사용은 직접적으로 인스턴스화 될 때 오류를 던지지만 다른 클래스를 통해 상속을 허용하는 추상 기본 클래스를 만드는 것입니다.
전반적으로 클래스는 JavaScript에 중요한 추가 요소입니다. 보다 간결한 구문과 사용자 정의 객체 타입을 안전하고 일관된 방식으로 정의하기 위한 더 나은 기능을 제공합니다.
'코딩' 카테고리의 다른 글
http-server 서버 실행 (0) | 2019.03.18 |
---|---|
javascript react : props 간단 설명 (0) | 2019.03.16 |
인스턴스 클래스 객체 instance class object (0) | 2019.03.09 |
javascript : async await callback promise (0) | 2019.03.09 |
javascript react : component LifeCycle API (0) | 2019.03.06 |