/ 본 글은 지적 호기심에 따른 탐구과정을 의식의 흐름 기법에 따라 적어내려간 것이므로 내용전개가 매끄럽지 못할 수 있음을 미리 밝힙니다. /

최근 모 커뮤니티에서 ‘ES6를 공부해야 할 필요는 없다’는 의견을 발견하였는데, 그 분의 견해를 읽던 중 ‘Class는 문법설탕일 뿐’이라는 내용에 대해서 정말로 그러한지 알아보고 싶은 마음이 들었습니다.

우선 구글링을 통해 ‘문법설탕’이라고 정의하고 있는 글들을 찾아보았습니다. 그 중 유명한 사이트의 글들을 일부만 발췌해왔는데, 이외에도 매우 많은 것으로 보아 이 견해가 거의 정론처럼 여겨지고 있는 것 같습니다.

JavaScript classes introduced in ECMAScript 6 are syntactical sugar over JavaScript’s existing prototype-based inheritance. The class syntax is not introducing a new object-oriented inheritance model to JavaScript. - MDN - Classes

Many features in ES6 (such as destructuring) are, in fact, syntactic sugar – and classes are no exception. - ES6 Classes in Depth

Not “traditional” classes, syntax sugar on top of prototypal inheritance - ES6 Overview in 350 Bullet Points

In basic use, the class keyword is syntactic sugar for writing constructor functions with prototypes. - JavaScript Allongé

심지어 ‘전적으로’ 문법설탕이라고 명확히 선을 긋는 사람들도 보입니다.

They are totally syntactical sugar. - are es6 classes just syntactic sugar for the prototypal pattern in javascript?

그런데 위 링크 중 대부분의 글들은 ‘문법설탕이다’에서 그치지 않고 곧바로 ‘하지만 더 엄격한 제약이 생겼다’는 등의 말을 덧붙이고 있습니다.

What is ‘Syntatic Sugar’?

16.10.07. 추가 - 하루동안의 반응을 지켜보니 ‘문법설탕’이라는 표현이 전혀 와닿지 않는게 저뿐은 아닌 것 같습니다. 일단은 업계에서 관용적으로 쓰이고 있는 이상 본문에서는 관례에 따르겠지만, ‘편의문법’으로 바꾸자는 목소리도 있으니 적절히 섞어 쓰면서 점차 바꿔나가는 것은 어떨까 싶기도 합니다.

wikipedia에서는 ‘syntactic sugar’를 다음과 같이 정의하고 있습니다.

Specifically, a construct in a language is called syntactic sugar if it can be removed from the language without any effect on what the language can do: functionality and expressive power will remain the same. - wikipedia

없는 영어실력으로 발번역 해보자면, 어떤 언어에서 제거하더라도 그 언어가 제공하는 기능과 표현력을 똑같이 유지하는 데에 아무런 노력이 필요하지 않은 구조 정도가 될 것 같습니다.

이 정의에 따르자면, 앞서 소개한 링크들 중 ‘ES5와 동일한 기능을 하긴 하지만 보다 엄격한 제약이 따른다’는 주장들이 사실일 경우 Class는 문법설탕이 아니라는 결론이 나오게 됩니다. ‘기능과 표현력은 그대로이지만 완전히 동일한 기능을 수행하지는 않는다’라는 모순된 문장이 만들어지기 때문이죠.

본 글은 문장과 글의 모순을 파헤치려는 목적이 아니므로 단어의 의미를 곱씹는 것은 이정도로 하고, 위 링크들이 말하고자 하는 바를 좀더 알아보고자 합니다. 과연 prototype과 class 사이에 어떤 차이가 있는지 하나하나 살펴보도록 하겠습니다.

constructor

1) new keyword

기본적으로 ES6 Class의 constructor는 기존의 ‘생성자함수’와 동일한 동작을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function ES5(name) {
this.name = name;
}
class ES6 {
constructor(name) {
this.name = name;
}
}
const es5 = new ES5('ES5');
const es6 = new ES6('ES6');
console.log(es5.name, es6.name); // ES5 ES6

그런데 ES5에서는 생성자로서의 기능과 일반 함수로서의 기능 모두를 수행할 수 있었던 반면, ES6의 constructor에 같은 방법을 시도하면 문제가 생깁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function ES5(name) {
this.name = name;
return name + ' es5';
}
class ES6 {
constructor(name) {
this.name = name;
return name + ' es6';
}
}
console.log(ES5('ES5')); // ES5 es5
console.log(ES5.prototype.constructor('ES5')); // ES5 es5
console.log(ES6('ES6')); // Uncaught TypeError
console.log(ES6.prototype.constructor('ES6')); // Uncaught TypeError

Chrome 개발자도구에서 확인한 오류메시지는 다음과 같습니다.

Uncaught TypeError: Class constructor ES6 cannot be invoked without ‘new’(…)

Class의 constructor는 ‘new’ 명령어 없이는 호출할 수 없다고 하는군요.
즉, Class의 constructor는 기존의 생성자함수와 달리 함수로서는 동작하지 않으며 오직 생성자로서만 존재할 수 있겠습니다.

혹시 모르니 확인을 해보죠.

1
2
3
4
5
console.log(new ES6('ES6')); // ES6 { name: "ES6" }
console.log(new ES6.prototype.constructor('ES6')); // ES6 { name: "ES6" }
const es6 = new ES6('ES6');
console.log(new es6.constructor('ES6 awesome')); // ES6 { name: "ES6 awesome" }

그렇다면 constructor 메소드 내의 return 값은 return 및 그 이후의 내용은 무시된다는 점을 제외하고는 의미가 없을 것입니다. 어떤 경우에도 return 구문으로부터 결과값을 반환받을 방법이 없을테니까요.

1
2
3
4
5
6
7
8
9
class ES6 {
constructor(name, age) {
this.name = name;
return name + ' es6';
this.age = age;
}
}
const es6 = new ES6('es6', 2015);
console.log(es6); // ES6 { name: "es6" }

2) super and extends keyword

기존 프로토타입 상속 하에서는 자식클래스의 생성자함수가 부모클래스의 생성자 함수의 내용을 덮어씌우는 식으로 동작하므로,
기본적으로 부모클래스의 생성자함수를 자식클래스의 생성자함수에서 호출한 것 같은 효과를 얻을 수 없습니다. (물론 프로토타입 체이닝을 통해 값은 전달되겠지만요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent() {
this.a = 1;
}
function Child() {
this.b = 2;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var obj = new Child();
console.log(obj.a, obj.b); // 1 2
console.log(obj.hasOwnProperty('a')); // false
console.log(obj.hasOwnProperty('b')); // true

만약 자식 클래스에서 부모 클래스의 생성자함수를 그대로 차용하여 실행하기를 원한다면 다음과 같은 복잡한 수행과정을 거쳐야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent(){
this.a = 1;
}
function Child(){
var parentObj = Object.getPrototypeOf(this);
for(let i in parentObj){
this[i] = parentObj[i];
}
this.b = 2;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var obj = new Child();
console.log(obj.a, obj.b); // 1 2
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // true

그러나 이 역시 완벽하지는 않습니다. 부모 클래스에도 여전히 ‘a’ 프로퍼티가 존재하기 때문이죠.
결국은 부모 클래스의 값을 복제한 것 이상의 의미를 지니지는 않습니다.

1
2
console.log(Object.getPrototypeOf(obj).a); // 1
console.log(Object.getPrototypeOf(obj).hasOwnProperty('a')); // true

한편 ES6에서는 extends 키워드로 subClass를 구현할 수 있는 길을 제공해주고 있으며,
이 경우 super 키워드를 통해 부모클래스(superClass)의 constructor를 자식클래스에서 호출할 수 있도록 구현하고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent {
constructor(){
this.a = 1;
}
}
class Child extends Parent {
constructor(){
super();
this.b = 2;
}
}
const obj = new Child();
console.log(obj.a, obj.b); // 1 2
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // true

또한 Child는 Parent의 인스턴스를 상속받는 것이 아닌 서브클래스로서 Parent의 메소드만을 상속받는 것이므로,
Child 인스턴스(obj)의 프로토타입 체인 상에도 Parent의 constructor의 실행 결과는 존재하지 않게 됩니다.

1
2
console.log(Object.getPrototypeOf(obj).a); // undefined
console.log(Object.getPrototypeOf(obj).hasOwnProperty('a')); // false

물론 ES5에서도 super/subClass를 구현할 수 있긴 하지만, 그러기 위해서는 다음처럼 꽤 복잡한 과정을 거쳐야 합니다.

1
2
3
4
5
6
function Parent() { this.a = 1; }
function Child() { this.a = 2; }
function Proxy() { }
Proxy.prototype = new Parent();
Child.prototype.constructor = Child;
Child.superClass = Parent.prototype;

위 방법은 임시생성자를 프록시로 활용하는 방법입니다. 위 내용을 토대로 프록시를 외부에 노출시키지 않기 위해 함수로 감싸고, 프록시를 자꾸 생산하지 않게끔 클로저로 감싸고, 최초 실행시부터 클로저가 발동하게끔 즉시실행함수로 감싸는 등을 추가로 수행한 것이 ‘임시생성자(프록시) 활용패턴’으로 알려져 있습니다. 이 프록시 활용패턴의 구현은 중급 자바스크립트 개발자를 위한 교재에서 자주 등장하는 단골 메뉴이기도 했죠.

1
2
3
4
5
6
7
8
9
10
11
12
var inherit = (function() {
function F(){ }
return function(C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.constructor.prototype = C;
C.superClass = P.prototype;
}
})();
function Parent() { this.a = 1; }
function Child() { this.a = 2; }
inherit(Child, Parent);

methods

1) static method 및 method의 상속

ES5에서 static method 및 method를 구현하려면 다음과 같이 작성해야 했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() { }
Parent.staticMethod = function() {
this.s = 11;
return 'static method';
}
Parent.prototype.method = function() {
this.m = 12;
return 'method';
}
function Child() { }
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var obj = new Child();

ES6 Class의 static method 및 method 선언 방식은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent {
static staticMethod() {
this.s = 11;
return 'static method';
}
method() {
this.m = 12;
return 'method';
}
}
class Child extends Parent { }
const obj = new Child();

상속관계를 되짚어가며 method를 출력해보면 양 쪽 결과가 같습니다.

1
2
3
console.log(obj.method()); // 'method'
console.log(Child.prototype.method()); // 'method'
console.log(Parent.prototype.method()); // 'method'

그러나 static method의 경우에는 결과가 다릅니다.

1
2
3
4
5
6
7
8
9
// ES5
console.log(obj.staticMethod()); // Uncaught TypeError
console.log(Child.staticMethod()); // Uncaught TypeError
console.log(Parent.staticMethod()); // 'static'
// ES6
console.log(obj.staticMethod()); // Uncaught TypeError
console.log(Child.staticMethod()); // 'static'
console.log(Parent.staticMethod()); // 'static'

앞서 언급한 프록시 생성자 활용패턴을 활용하더라도, superClass의 static method를 subClass에서도 호출 가능하게 하기 위해서는 추가로 해주어야 할 게 있습니다. 즉 Parent의 메소드를 Child에 복사하는 것이지요. 단, Child에 같은 이름의 static method가 있다면 Parent의 static method가 덮어씌우게 되니, 그러지 않게끔 접두어나 접미어를 붙여야 합니다. 혹은 프로토타입 체이닝을 통해 직접 superClass에 접근하면 되겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var inherit = (function() {
function F(){ }
return function(C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.constructor.prototype = C;
C.superClass = P.prototype;
for(var static in P) {
C['super_' + static] = P[static];
}
}
})();
function Parent() { this.a = 1; }
Parent.method = function(){ console.log('super static'); };
function Child() { this.a = 2; }
Child.method = function(){ console.log('sub static'); };
inherit(Child, Parent);
Child.method(); // sub static method
Child.super_method(); // super static method
Child.superClass.constructor.method(); // super static method

이밖에도 Klass 등의 ‘흉내’낸 패턴들이 존재하긴 하지만, 어느 방법에 의하더라도 static method는 ‘복사’하는 외엔 마땅한 대안이 없을 뿐 아니라,
subClass의 static method 내에서 superClass의 다른 method를 이용하려면 또다른 방법을 잔뜩 강구해야만 합니다.

2) 생성자 함수

ES5의 static method 및 method는 그 자체로 함수이기 때문에 별개의 생성자 함수로 사용할 수 있습니다.

1
2
3
4
var methodObj = new ES5Parent.prototype.method();
var staticObj = new ES5Parent.staticMethod();
console.log(staticObj); // P…t.staticMethod {s: 11}
console.log(methodObj); // P…t.method {m: 12}

반면 ES6의 static method 및 method는 생성자함수로 활용할 수 없습니다.

1
2
3
4
var methodObj = new ES6Parent.prototype.method();
var staticObj = new ES6Parent.staticMethod();
console.log(staticObj); // Uncaught TypeError
console.log(methodObj); // Uncaught TypeError

그 원인은 ES5와 ES6의 각 메소드를 개발자도구로 출력해보면 파악할 수 있습니다.

ES5 static method vs. ES6 static method

그림에서 드러나듯이 ES6의 staticMethod에는 arguments, caller 값이 노출되지 않고 있고, name이 자동으로 지정되어 있으며, prototype이 없습니다. ES6의 shorthand method는 본질적으로는 function에 해당하긴 하지만, 기존 function에서 많은 기능이 제한되어 오직 method로서만 사용할 수 있는 특수한 함수입니다.

3) superClass 메소드 차용

constructor 파트에서 이미 어느정도 감은 잡으셨겠지만, 보다 확실히 하는 차원에서 method 차용에 대해서도 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() { }
Parent.prototype.method = function() {
return 'super';
}
function Child() { }
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.method = function() {
return Object.getPrototypeOf(this).method() + ' sub';
}
var obj = new Child();
console.log(obj.method()); // 'super sub sub'

ES5에서 부모클래스와 자식클래스에 동일한 메소드를 정의한 경우, 부모클래스의 메소드를 자식클래스에서 호출하려면 위와 같은 탐색과정을 거쳐야 합니다. 그마저도 인스턴스에서 method를 호출하면 __proto__에 위치한 메소드를 마치 인스턴스 자신의 것처럼 사용하기 때문에, 위 메소드에는 최초 실행시 this에는 ‘obj’가 할당되었다가, 재귀적으로 Child.prototype가 할당되었다가, 다시 Parent.prototype이 할당되기 때문에, 본래 의도한 ‘super sub’라는 결과 대신 ‘ sub’가 한 번 더 출력되고 말았습니다. 이 문제를 해결하기 위해서는 조건을 설정하는 등의 노력이 추가되어야 할 것입니다.

this를 사용할 경우의 문제점은 ES6 클래스에서도 똑같이 확인됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Parent {
method() {
return 'super';
}
}
class Child extends Parent {
method() {
return Object.getPrototypeOf(this).method() + ' sub';
}
}
var obj = new Child();
console.log(obj.method()); // 'super sub sub'

그러나 ES6에는 super 키워드가 있어서, this 활용시의 문제점을 피해갈 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Parent {
method() {
return 'super';
}
}
class Child extends Parent {
method() {
return super.method() + ' sub';
}
}
var obj = new Child();
console.log(obj.method()); // 'super sub'

super 키워드는 상위클래스’만’을 가리키므로 재귀적으로 여러번 호출될 염려 없이 오직 Parent Class의 메소드만을 상속받아 활용할 수 있습니다.

hoisting

제가 읽은 거의 모든 자료들은 하나같이 클래스는 호이스팅이 일어나지 않는다고 하고 있습니다. 그 근거로 클래스 선언 전에 해당 클래스를 호출하면 Reference Error를 던지고, 블록 스코프의 영향을 받는 등을 들고 있습니다. 아래는 그 중 몇 개를 발췌한 것이며, 이외에도 굉장히 많습니다.

class declarations are not hoisted. Therefore, a class only exists after execution reached its definition and it was evaluated. Accessing it beforehand leads to a ReferenceError - Exploring ES6, 2ality

An important difference between function declarations and class declarations is that function declarations are hoisted and class declarations are not. You first need to declare your class and then access it, otherwise code like the following will throw a ReferenceError - MDN

The difference between class declarations and function declarations is that functions are hoisted and classes are not. - stackAbuse

class declarations can’t be hoisted. - StrongLoop

Class declarations are not hoisted as function declarations are. - freeCodeCamp

반면 ‘hoisting은 일어난다’는 내용은 오직 stackOverflow의 몇몇 질문에 대한 대답에서만 발견할 수 있었습니다.

Why are ES6 classes not hoisted?

Are variables declared with let or const not hoisted in ES6?

호이스팅이 발생하느냐 발생하지 않느냐에 대한 확인은 뒤로 미루고, 일단 위의 ‘근거’ 두 가지가 모두 사실인지부터 확인해 봅시다. 그런 후에 다시 호이스팅에 대해 다루도록 하겠습니다.

1) ReferenceError

1
2
3
4
5
6
// ES5
var es5 = new ES5();
function ES5() {
this.a = 1;
}
console.log(es5); // ES5 {a: 1}
1
2
3
4
5
6
7
8
// ES6
const es6 = new ES6();
class ES6 {
constructor() {
this.a = 1;
}
}
console.log(es6); // Uncaught ReferenceError

ES6 Class는 ES5의 생성자함수와 달리 선언 전에 미리 인스턴스를 생성하고자 할 때 ReferenceError가 발생하는 것이 확인되었습니다.

2) Block Scope

ES5의 함수는 ES6의 strict mode에서만 block scope의 영향을 받습니다.

1
2
3
4
5
6
function A(){ this.a = 1; }
{
function A(){ this.a = 2; }
console.log(new A()); // A {a: 2}
}
console.log(new A()); // A {a: 2}
1
2
3
4
5
6
7
'use strict';
function A(){ this.a = 1; }
{
function A(){ this.a = 2; }
console.log(new A()); // A {a: 2}
}
console.log(new A()); // A {a: 1}

한편 ES6의 Class는 언제나 block scope에 종속됩니다.

1
2
3
4
5
6
7
8
9
10
class A {
constructor(){ this.a = 1; }
}
{
class A {
constructor(){ this.a = 2; }
}
console.log(new A()); // A {a: 2}
}
console.log(new A()); // A {a: 1}

간단하게 확인이 되었네요. 정리해보면, 클래스는 선언 전에 해당 클래스의 인스턴스를 생성하려 하면 ReferenceError를 던지며, ‘strict mode’ 여부에 무관하게 언제나 block scope의 영향 하에서만 존재할 수 있습니다.

3) hoisting

다시 한 번 호이스팅을 살펴봅시다.

tc39는 var에 대해 다음과 같이 정의하고 있습니다.

A var statement declares variables that are scoped to the running execution context’s VariableEnvironment. Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created. - Variable statement

한편 let, const에 대해서는 다음과 같이 정의하고 있습니다.

let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. - let and const declarations

위의 정의를 요약해보면 다음과 같습니다. varLexicalEnvironment 가 형성될 때 함께 생성되면서 동시에 undefined로 초기화됩니다. 반면 letconstLexicalEnvironment 가 형성될 때 함께 생성되지만, 이들의 LexicalBinding 이 평가되기 전까지는 접근할 수 없으며, LexicalBinding이 평가되는 시점에 비로소 초기값이 할당됩니다.

여기서 ‘Lexical Environment가 형성될 때 함께 생성됨’ 부분이 hoisting의 정의입니다. var의 ‘undefined 할당’과 letconst의 ‘LexicalBinding이 평가되기 전까지는 접근 불가’ 부분은 hoisting과는 별도로 분리하여 논해야 합니다. 그리고 이러한 ‘접근불가’영역을 통상 TDZ(Temporal Dead Zone, 임시사각지대)라고 부릅니다.

TDZ에 대한 자세한 내용은 아래 링크를 참조하세요.

TDZ 영역에서 값을 얻으려 하면 ‘ReferenceError’ 오류를 던집니다. Chrome의 오류메시지는 다음과 같습니다.

1
2
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 1;

위 예제를 놓고 ‘hoisting이 일어나지 않은 것과 다를 바 없지 않느냐’고 생각하실 수도 있겠으나, 다음과 같은 상황에선 생각이 달라지실 것입니다.

1
2
3
4
5
let a = 1;
{
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 2;
}

‘hoisting’이 일어나지 않은 상황이라면 전역스코프에서 선언한 a변수의 값 1이 출력되었어야 마땅한 곳에서 ‘ReferenceError’가 발생하고 있습니다. 새로운 블록스코프가 생성됨과 동시에 해당 스코프 내의 a 변수가 생성되었으나, 아직 변수에 초기값이 할당되기 전 상태이기 때문에 ReferenceError를 던지는 것이죠.

앞서 “var의 ‘undefined 할당’과 letconst의 ‘LexicalBinding이 평가되기 전까지는 접근 불가’ 부분은 hoisting과는 별도로 분리하여 논해야 한다” 라고 말씀드린 이유가 바로 이것 때문입니다. ES5 환경에서는 변수가 var만 존재했으므로(함수선언문은 논외) hoisting과 undefined 할당을 동일시해도 문제되지 않았으나, 새로운 변수 유형이 생긴 ES6 환경에서는 이를 분리할 필요성이 생긴 것입니다.

뜬금없이 웬 변수 얘기를 하고 있냐면, 위 둘의 차이가 functionclass 사이에도 똑같이 발견되고 있어서 그렇습니다.

1
2
3
4
5
function A(){ this.a = 1; }
{
console.log(new A()); // A {a: 2}
function A(){ this.a = 2; }
}
1
2
3
4
5
6
7
8
9
class A {
constructor(){ this.a = 1; }
}
{
console.log(new A()); // Uncaught ReferenceError: A is not defined
class A {
constructor(){ this.a = 2; }
}
}

class가 선언되기 전 위치에서는 ‘ReferenceError’를 던질 뿐 아니라 class가 block scope의 영향 하에 있는 것에서 미루어보면, hoisting과 관련해서는 classletconst와 동일하게 취급하고 있다고 볼 수 있겠습니다.

정리

기능 ES5 ES6
constructor를 함수로 실행 O X
superClass의 constructor 호출 X(기본). 흉내는 가능하나 한계가 있음 O. super 키워드
methods 상속 O O
methods를 생성자함수로 실행 O X
static methods : 상속 X(기본). 흉내는 가능하나 한계가 있음 O
static methods를 생성자함수로 실행 O X
methods: superClass의 메소드 호출 X(기본). 흉내는 가능하나 한계가 있음 O. super 키워드
hoisting O O
TDZ X O

마치며

ES6의 클래스가 ‘prototype’ 상속의 ‘문법설탕’에 불과한지 여부를 확인하기 위해 먼 길을 돌아왔습니다. 기존의 생성자함수와 ES6의 Class는 ‘prototype’을 이용하여 상속관계를 구현했다는 점에서는 같으나 구체적인 성질은 전혀 다른 부분이 많습니다. 특히 super 키워드로 처리 가능한 것들을 기존 방식으로 구현하기 위해서는 매우 많은 노력이 필요할뿐 아니라, subClass 메소드가 superClass 메소드를 호출한 다음 다시 자신만의 내용을 이어갈지 여부는 구체적 상황에 따라 달라질 수밖에 없는 문제이니 이를 패턴화하기도 어려워 보입니다. 기존 생성자함수가 하던 형태를 그대로 유지하면서 Class로 전환하는 것은 가능하고 어렵지 않을 것 같지만, 반대로 ES6의 Class를 기존 방식으로 전환하는 건 어렵거나 불가능에 가까운 경우도 존재할 것 같습니다.

ES6는 기본적으로 하위호환성을 유지한 채로 새로운 기능을 추가하면서도 언어가 갑자기 너무 비대해지는 것은 막아야 했기에, 최대한 새로운 기능들이 기존 기능을 토대로 동작하도록 구현되었습니다. Class가 prototype 상속을 차용한 것은 이러한 One Javascript 전략에 따르기 위한 것이었으며, 기존 문법으로는 구현할 수 없는 기능도 포함되어 있는 이상 ‘문법설탕’ 취급을 하는 것은 너무 야박한 평가가 아닌가 싶네요. ㅎㅎ

중간중간 모르는 내용을 근거도 없이 추측하거나 추정했을 수도 있고, 잘못된 내용을 단정적으로 확신하며 언급했을 가능성도 있을 것 같습니다. 혹시라도 틀렸거나 보충이 필요한 내용이 있다면 기탄없이 알려주시면 감사하겠습니다.