먼저 자바스크립트는 프로토타입 기반 언어라서 '상속' 개념이 존재하지 않는다.
이는 클래스 기반의 다른 언어에 익숙한 많은 개발자들을 혼한스럽게 했다.
따라서 클래스와 비슷하게 동작하게끔 흉내 내는 여러 기법들이 탄생하였다.
또한 ES6에는 클래스 문법이 추가되었다.
다만 ES6의 클래스에서도 일정 부분은 프로토타입을 활용하고 있기 때문에 ES5체제 하에서 클래스를 흉내내기 위한 구현 방식을 알아보겠다.
자바스크립트의 클래스
자바스크립트는 프로토타입 기반 언어이므로 클래스의 개념이 존재하지 않는다. 그렇지만 프로토타입을 일반적인 의미에서의 클래스 관점에서 접근해보면 비슷하게 해석할 수 있는 요소가 없지 않다.
예시로 생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성된다. 이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 '상속'된다고 볼 수 있다.
엄밀하게는 상속이 아닌 프로토타입 체이닝에 의한 참조이다.
또한, 인스터스에 상속되는지(인스턴스가 참조하는지) 여부에 따라 스태틱 멤버와 인스턴스 멤버로 나뉜다.
여느 클래스 기반 언어와 달리 자바스크립트에서는 인스턴스에서도 직접 메서드를 정의할 수 있기 때문에 '인스턴스 메서드'라는 명칭은 프로토타입에 정의한 메서드를 지칭하는 것인지 인스턴스에 정의한 메서드를 지칭하는 것인지에 대해 도리어 혼란을 야기한다. 따라서 이 명칭 대신에 자바스크립트의 특징을 살려 프로토타입 메서드라고 부르는게 더 좋아보이고 실제로도 많은 개발자들이 사용하고 있다.
기본적인 구현과 이 구현에서의 발생하는 문제점
var Grade = function() {
var args = Array.prototype.slice.call(argument);
for (var i = 0; i < args.length; i++){
this[i] = args[i];
}
this.length = args.length;
};
Grade.prototype = [];
var g = new Grade(100, 80);
ES5까지의 자바스크립트에는 클래스가 없다. 또한, ES6에서 클래스가 도입됐지만 역시난 prototype을 기반으로 한 것이다.
이렇게 class를 구현하면 생기는 문제점이 몇개 존재한다.
먼저 length에 대한 예시를 보겠다.
...
g.push(90);
console.log(g); // Grade { 0 : 100, 1 : 80, 2 : 90, length : 3}
delete g.length;
g.push(70);
console.log(g); // Grade { 0: 70, 1 : 80, 2: 90, length : 1}
첫번째는 원하는 대로 결과가 잘 나왔다. 그런데 두번째에서 length프로퍼티를 삭제하고 다시 push를 하였더니, push한 값이 0번째 인덱스에 들어갔고, lengt가 1이 되었다.
이유는 무엇일까?
바로 g.__proto__ 즉, Grade.prototype이 빈 배열을 가리키고 있기 때문이다.
push 명령에 의해 자바스크립트 엔진이 g.length를 읽고자 하는데 g.length가 없으니까 프로토타입 체이닝을 타고 g.__proto__.length을 읽어 온 것이다. 빈 배열ㅇ의 length가 0이므로 여기에 값을 할당하고 length는 1만큼 증가시키라는 명령이 문제없이 동작할 수 있었던 것이다.
다시말해, 클래스에 있는 값이 인스턴스에 영향을 줄 수 있는 구조가 문제이다.
클래스가 구체적인 데이터를 지니지 않게 하는 방법
클래스(prototype)가 구체적인 데이터를 지니지 않게 하는 방법은 여러 가지가 있는데, 그 중 가장 쉬운 방법은 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 것이다.
클래스 상속 및 추상화 방법(1) - 인스턴스 생성 후 프로퍼티 제거
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for ( var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
if (SubMethods) {
for ( var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
var Square = extendClass1(Rectangle, function (width) {
Rectangle.call(this, width, width)
});
다른 방법은 SubClass의 prototype에 직접 SuperClass의 인스턴스를 할당하는 대신에 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 하나 더 만들어서 그 prototype 이 SuperClass의 prototype을 바라보게끔 한 다음, SubClass의 prototype에는 Bridge의 인스턴스를 할당하게 하는 것이다.
클래스 상속 및 추상화 방법(2) - 빈 함수를 활용
var extendClass2 = (function () {
var Bridge = function() {}; // 빈 함수 생성
return function ( SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge() // SubClass에 빈 함수 할당
if (subMethods) {
for ( var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
})();
즉시실행함수 내부에서 Bridge를 선언해서 이를 클로저로 활용함으로써 메모리에 불필요한 함수 선언을 줄였다.
subMethods에는 SubClass의 prototype에 담길 메서드들을 객체로 전달하게끔 했다.
마지막으로 ES5에서 도입된 Object.create를 이용한 방법이 있다.
이 방법은 SubClass의 prototype의 __proto__가 SuperClass의 prototype을 바라보되, SuperClass의 인스턴스가 되지는 않으므로 앞서 소개한 두 방법보다 간단하면서 안전하다.
클래스 상속 및 추상화 방법(3) - Object.create 활용
// (...생략)
Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);
// (...생략)
constructor 복구하기
위 세 가지 방법 모두 기본적인 상속에는 성공했지만 SubClass 인스턴스의 constructor는 여전히 SuperClass를 가리키는 상태이다. 엄밀히는 SubClass 인스턴스에는 constructor가 없고, SubClass.prototype에도 없는 상태이다. 프로토타입 체인상에 가장 먼저 등장하는 SuperClass.prototype의 constructor에서 가리키는 대상, 즉 SuperClass가 출력될 뿐이다.
따라서, 위 코드들의 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주면 된다.
클래스 상속 및 추상화 방법(1) - 인스턴스 생성 후 프로퍼티 제거 - 완성본(constructor 복구)
var extendClass1 = function (SuperClass, SubClass, subMethods) {
SubClass.prototype = new SuperClass();
for ( var prop in SubClass.prototype) {
if (SubClass.prototype.hasOwnProperty(prop)) {
delete SubClass.prototype[prop];
}
}
SubClass.prototype.constructor = SubClass; // 추가된구문
if (SubMethods) {
for ( var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
var Square = extendClass1(Rectangle, function (width) {
Rectangle.call(this, width, width)
});
클래스 상속 및 추상화 방법(2) - 빈 함수를 활용 - 완성본 (constructor 복구)
var extendClass2 = (function () {
var Bridge = function() {}; // 빈 함수 생성
return function ( SuperClass, SubClass, subMethods) {
Bridge.prototype = SuperClass.prototype;
SubClass.prototype = new Bridge() // SubClass에 빈 함수 할당
SubClass.prototype.constructor = SubClass; // 추가된 구문
if (subMethods) {
for ( var method in subMethods) {
SubClass.prototype[method] = subMethods[method];
}
}
Object.freeze(SubClass.prototype);
return SubClass;
};
})();
클래스 상속 및 추상화 방법(3) - Object.create 활용 - 완성본 (constructor 복구)
// (...생략)
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square // 추가된구문
Object.freeze(Square.prototype);
// (...생략)
ES6에서의 Class
Class 정의
class는 사실 "특별한 함수"이다. 함수를 함수 표현식과 함수 선언으로 정의할 수 있듯이. class 문법도 class 표현식 and class 선언 두 가지 방법을 제공한다.
Class 선언
class를 정의하는 한 가지 방법은 class 선언을 이용하는 것이다.
class를 선언하기 위해서는 클래스의 이름(여기서 "Rectangle")과 함께 class 키워드를 사용해야 한다.
class Rectangle {
constructor( height, width) {
this.height = height;
this.width = width;
}
}
Hoisting
함수 선언과 클래스 선언의 중요한 차이점은 함수의 경우 정의하기 하기 전에 호출할 수 있지만, 클래스는 반드시 정의한 뒤에 사용할 수 있다는 점이다. 다음 코드는 ReferenceError를 던질 것이다.
const p = new Rectangle(); // ReferenceError
class Rectangle {}
예외가 발생하는 이유는 클래스가 호이스팅될 때 초기화는 되지 않기 때문이다.(let, const와 같다.)
Class 표현식
Class 표현식은 class를 정의하는 또 다른 방법이다. Class 표현식은 이름을 가질 수도 있고, 갖지 않을 수도 있다.
이름을 가진 class 표현식의 이름은 클래스의 body의 local scope에 한해 유효하다.
// unnamed
let Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 출력: "Rectangle"
// named
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 출력: "Rectangle2"
클래스 표현식에는 Class 선언에 설명된 것과 동일한 호이스팅 제한이 적용된다.
Class body 와 메서드 정의
Class body는 중괄호 {} 로 묶여 있는 안쪽 부분이다.
이 곳은 메서드나 constructor와 같은 class 멤버를 정의할 곳이다.
Strict mode
클래스의 본문(body)은 strict mode에서 실행된다. 즉, 여기에 적힌 코드는 성능 향상을 위해 더 엄격한 문법이 적용된다.
그렇지 않으면, 조용히 오류가 발생 할 수 있다.
Constructor(생성자)
constructor 메서드는 class로 생성된 객체를 생성하고 초기화하기 위한 특수한 메서드이다.
"constructor"라는 이름을 가진 특수한 메서드는 클래스 안에 한 개만 존재할 수 있다.
만약 클래스에 여러 개의 constructor 메서드가 존재하면 SyntaxError가 발생 할 것이다.
constructor는 부모 클래스의 constructor를 호출하기 위해 super 키워드를 사용할 수 있다.
프로토타입 메서드
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea();
}
// 메서드
calcArea() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.area); // 100
정적 메서드와 속성
static 키워드는 클래스를 위한 정적(static) 메서드를 정의한다. 정적 메서드는 클래스의 인스턴스화 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없다. 정적 메서드는 어플리케이션을 위한 유틸리티 함수를 생성하는 데 주로 사용된다.
반면, 정적 속성은 캐시, 고정 환경설정 또는 인스턴스 간에 복제할 필요가 없는 기타 데이터에 유용하다.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static displayName = "Point";
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
}
const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined
console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755
프로토타입 및 정적 메서드를 사용한 this 바인딩
메서드를 변수에 할당 한 다음 호출하는 것과 같이, 정적 메서드나 프로토타입 메서드가 this 값 없이 호출될 때, this 값은 메서드 안에서 undefined가 된다. 이 동작은 "use strict" 명령어 없이도 같은 방식으로 동작하는데, class 문법 안에 있는 코드는 항상 strict mode로 실행되기 때문이다.
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
let obj = new Animal();
obj.speak(); // the Animal object
let speak = obj.speak;
speak(); // undefined
Animal.eat(); // class Animal
let eat = Animal.eat;
eat(); // undefined
위에 작성된 코드를 전통적 방식의 함수기반의 non-strict mode 구문으로 재작성하면, this 메서드 호출은 기본적으로 전역 객체인 초기 this값에 자동으로 바인딩된다. Strict mode에서는 자동 바인딩이 발생하지 않는다. this값은 전달된 대로 유지된다.
function Animal() {}
Animal.prototype.speak = function () {
return this;
};
Animal.eat = function () {
return this;
};
let obj = new Animal();
let speak = obj.speak;
speak(); // global object (in non–strict mode)
let eat = Animal.eat;
eat(); // global object (in non-strict mode)
인스턴스 속성
인스턴스 속성은 반드시 클래스 메서드 내에 정의되어야 한다.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
정적 (클래스사이드) 속성과 프로토타입 데이터 속성은 반드시 클래스 선언부 바깥쪽에서 정의되어야 한다.
Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;
Public 필드 선언
JavaScript의 필드 선언 문법을 사용해서 위의 예제는 아래와 같이 다시 쓰여질 수 있다.
class Rectangle {
height = 0;
width;
constructor(height, width) {
this.height = height;
this.width = width;
}
}
필드를 먼저 선언함으로서 클래스 정의는 self-documenting에 가까워졌고 필드는 언제나 존재하는 상태가 된다.
위의 예에서 봤듯이 필드 선언은 기본 값과 같이 선언될 수도 있다.
Private 필드 선언
private 필드를 사용하면 아래와 같이 예제를 개선할 수 있다.
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
클래스의 바깥에서 private 필드를 접근하려고 하면 에러가 발생한다. private필드는 클래스 내부에서만 읽고 쓰기가 가능하다. 클래스 외부에서 보이지 않도록 정의하였으므로 클래스가 버젼업 되면서 내부 구현이 바뀌더라도 클래스 사용자 입장에서는 이에 아무런 영항을 받지 않도록 할 수 있습니다.
Note: Private 필드는 사용전에 선언되어야 합니다.
일반적인 프로퍼티와는 다르게 private 필드는 값을 할당하면서 만들어질 수 없습니다.
extends를 통한 클래스 상속 (sub classing)
extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 클래스의 자식 클래스를 생성하기 위해 사용된다.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // super class 생성자를 호출하여 name 매개변수 전달
}
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog("Mitzie");
d.speak(); // Mitzie barks.
subclass에 constructor가 있다면, "this"를 사용하기 전에 가장 먼저 super()를 호출해야 합니다.
또한 es5에서 사용되던 전통적인 함수 기반의 클래스를 통하여 확장할 수도 있습니다.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} makes a noise.`);
};
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog("Mitzie");
d.speak(); // Mitzie barks.
// 유사한 메서드의 경우, 자식의 메서드가 부모의 메서드보다 우선합니다
클래스는 생성자가 없는 객체(non-constructible)을 확장할 수 없습니다. 만약 기존의 생성자가 없는 객체을 확장하고 싶다면, 이 메서드를 사용하세요.
const Animal = {
speak() {
console.log(`${this.name} makes a noise.`);
},
};
class Dog {
constructor(name) {
this.name = name;
}
}
// 이 작업을 수행하지 않으면 speak를 호출할 때 TypeError가 발생합니다
Object.setPrototypeOf(Dog.prototype, Animal);
let d = new Dog("Mitzie");
d.speak(); // Mitzie makes a noise.
Species
배열을 상속 받은 MyArray 클래스에서 Array Object를 반환하고 싶을 수도 있을 것입니다. 그럴때 Species 패턴은 기본 생성자를 덮어쓰도록 해줍니다.
예를 들어, map()과 같은 기본 생성자를 반환하는 메서드를 사용할 때 이 메서드가 MyArray 객체 대신 Array 객체가 반환하도록 하고 싶을 것입니다.
Symbol.species (en-US) 심볼은 이러한 것을 가능하게 해줍니다
class MyArray extends Array {
// 부모 Array 생성자로 species 덮어쓰기
static get [Symbol.species]() {
return Array;
}
}
var a = new MyArray(1, 2, 3);
var mapped = a.map((x) => x * x);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
super를 통한 상위 클래스 호출
super 키워드는 객체의 부모가 가지고 있는 메서드를 호출하기 위해 사용됩니다.
이는 프로토타입 기반 상속보다 좋은 점 중 하나입니다.
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(`${this.name} roars.`);
}
}
let l = new Lion("Fuzzy");
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.
Mix-ins
추상 서브 클래스 또는 믹스-인은 클래스를 위한 템플릿입니다. ECMAScript 클래스는 하나의 단일 슈퍼클래스만을 가질 수 있으며, 예를 들어 툴링 클래스로부터의 다중 상속은 불가능합니다. 기능은 반드시 슈퍼클래스에서 제공되어야 합니다.
슈퍼클래스를 인자로 받고 이 슈퍼클래스를 확장하는 서브클래스를 생성하여 반환하는 함수를 사용하여 ECMAScript에서 믹스-인을 구현할 수 있습니다
var calculatorMixin = (Base) =>
class extends Base {
calc() {}
};
var randomizerMixin = (Base) =>
class extends Base {
randomize() {}
};
위 믹스-인을 사용하는 클래스는 다음과 같이 작성할 수 있습니다
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}
클래스 재정의
클래스는 재정의 될 수 없습니다. 재정의를 시도하면 SyntaxError가 발생합니다.
'javascript Deep Dive' 카테고리의 다른 글
[Javascript] Javascript vs Java 웹사이트 채택이유 (1) | 2023.12.28 |
---|---|
[Javascript] V8 엔진 (0) | 2023.12.27 |
[JavaScript] prototype (0) | 2023.12.23 |
[JavaScript] 클로저(Closure) (0) | 2023.12.22 |
[JavaScript] Callback 함수 (0) | 2023.12.22 |