자바스크립트 완벽가이드 9장 (클래스와 모듈)

자바스크립트에서 클래스는 프로토타입 기반의 상속 메커니즘을 기반으로 하고있다. 두 객체가 같은 프로토타입 객체로부터 프로퍼티를 상속받았다면, 둘은 같은 클래스의 인스턴스다. 자바스크립트의 클래스와 프로토타입 기반 상속 메커니즘은 자바나 그와 비슷한 언어의 클래스 상속과는 상당히 다르다. 자바스크립트 클래스의 중요한 특징 중 하나는 동적으로 확장될 수 있다는 것이다. 클래스를 정의한 다는 말은 모듈화되고 재사용 가능한 코드를 작성한다는 뜻이다.

9.1 클래스와 프로토타입

자바스크립트의 클래스는 같은 프로토타입 객체로부터 프로퍼티를 상속받은 객체의 집합이다. 따라서 프로토타입 객체는 클래스의 핵심이다.


9.2 클래스와 생성자

생성자는 새로 생성된 객체를 초기화하는 용도로 사용되는 함수다. 생성자는 new 키워드를 사용하여 호출한다. 생성자를 호출하면 자동으로 새로운 객체가 생성되고, 생성자 함수 내부에서 새로 생성된 객체를 사용하기 때문에, 생성자 함수는 새 객체의 상태를 초기화하는 데만 신경 쓰면 된다. 생성자 호출의 핵심적인 특징은 생성자의 prototype 프로퍼티가 새 객체의 프로토타입으로 사용된다는 것이다. 이는 한 생성자를 통해 생성된 모든 객체는 같은 객체를 상속하고, 따라서 같은 클래스의 멤버임을 뜻한다.
클래스와 생성자 함수의 이름은 대문자로 시작하는 것은 매우 일반적은 코딩 규칙이다. 일반 함수와 메서드는 소문자로 이름을 시작한다.
새 객체는 생성자 함수가 실행되기 전에 자동으로 생성되고, 생성자 함수 내에서 this 값으로 접근할 수 있다. 생성자는 그저 새 객체를 초기화하기만 하면 되고 생성된 객체를 반환할 필요도 없다. 생성자를 호출하면 새 객체는 자동으로 생성되고, 새 객체의 메서드로서 생성자 함수가 호출된 다음, 초기화가 완료된 새 객체가 반환된다. 생성자 호출이 일반적인 함수 호출과 크게 다른것이 생성자 이름의 첫 글자를 대문자로 하는 또 하나의 이유다. 생성자는 new 키워드를 사용하여 호출된다고 가정하기 때문에, 일반적인 함수 호출처럼 호출하면 보통 제대로 작동하지 않는다.

9.2.1 생성자와 클래스 구별

프로토타입 객체는 클래스를 구별할 때 핵심적인 역할을 한다. 두 객체는 같은 프로토타입 객체를 상속한 경우에만 같은 클래스의 인스턴스다. 새로 생성된 객체의 상태를 초기화하는 생성자 함수는 클래스 구별의 핵심이 아니다. 서로 다른 두 생성자 함수라도 같은 프로토타입 객체를 가리키는 prototype 프로퍼티를 가질 수 있다. 그러면 두 생성자는 같은 클래스의 인스턴스를 만드는데 사용될 수 있다.
생성자가 prototype 만큼 객체 구별에 핵심적인 역할을 하지는 않더라도, 생성자는 클래스를 대표하는 역할을 한다. 생성자는 객체가 어떤 클래스에 속한 것인지 검사할 때 instanceof 연산자와 같이 사용된다.

1
r instanceof Rnage            // r이 Range.prototype을 상속했다면 true를 반환한다.

instanceof 연산자는 실제로 r이 Range 생성자에 의해 초기화되었는지를 검사하지는 않고, r이 Range.prototype을 상속하는지를 검사한다.

9.2.2 constructor 프로퍼티

모든 자바스크립트 함수는 생성자로 사용될 수 있는데, 함수가 생성자로 호출되려면 prototype 프로퍼티가 있어야 한다. 따라서 모든 자바스크립트 함수에는 자동으로 prototype 프로퍼티가 설정된다. 이 prototype 프로퍼티의 값은 constructor 프로퍼티 하나만 가진 객체다. constructor 프로퍼티는 열거되지 않으며 constructor 프로퍼티의 값은 해당 함수 객체다.

1
2
3
4
var F = function() {};      // 함수 객체다.
var p = F.prototype; // F와 연관이 있는 프로토타입 객체다.
var c = p.constructor; // 프로토타입과 관련한 함수 객체다.
c === F // => true: 모든 함수에 대해 F.prototype.constructor==F이다.

미리 정의된 프로토타입 객체가 있고 이 프로토타입 객체가 constructor 프로퍼티를 갖고 있다는 말은, 일반적으로 어떤 객체가 자기 자신의 생성자를 가리키는 constructor 프로퍼티 또한 상속하고 있음을 뜻한다.
별도로 정의한 프로토타입 객체에는 constructor 프로퍼티가 없다. 따라서 해당 클래스의 인스턴스에도 constructor 프로퍼티는 없을 것이다. 이 문제는 명시적으로 프로토타입 객체에 constructor 프로퍼티를 추가함으로써 해결한다. 일반적인 또 다른 기법은 constructor 프로퍼티가 미리 정의되어 있는 prototype 객체를 사용하는 것이다. 거기에 하나씩 메서드를 추가해가면 된다.


9.3 자바 스타일 클래스

자바스크립트가 자바와 다른 점 한가지는 함수가 값이라는 점이고, 따라서 메서드와 필드 사이에는 뚜렷한 구분이 없다. 프로퍼티 값이 함수라면 그 프로퍼티는 메서드이고, 함수가 아니라면 보통의 프로퍼티나 ‘필드’일 뿐이다.
자바스크립트가 자바 스타일의 클래스 멤버를 흉내 낼 수 있지만, 자바의 중요한 특징 중 자바스크립트가 지원하지 않는 것이 몇 가지 있다. 먼저 자바는 인스턴스 메서드 안에서 인스턴스 필드를 메서드의 지역 변수처럼 사용할 수 있고, this를 비롯한 어떤 접두사도 붙일 필요가 없다. 자바스크립트는 이를 지원하지 않지만, with문을 사용하여 비슷한 효과를 얻을 수 있다.(권장X) 자바에서는 final을 사용하여 상수 필드를 정의할 수 있다. 그리고 클래스 내부에서만 사용하고 외부에서 볼 수 없는 필드나 메서드는 private으로 정의할 수 있다. 자바스크립트에는 이런 키워드들이 없다. 따라서 힌트를 제공하는 표기 규칙을 사용한다.(값이 변경되면 안 되는 프로퍼티들은 이름이 대문자이고, 밑줄로 시작하는 이름의 프로퍼티는 클래스 외부에서 사용하면 안된다는 뜻이다.)
private 프로퍼티는 클로저의 지역 변수로 흉내 낼 수 있고, 상수 프로퍼티는 ECMAScript 5에서는 사용 가능하다.


9.4 클래스 확장하기

자바스크립트의 프로토타입 기반 상속 메커니즘은 동적이다. 객체는 자신의 프로토타입에서 프로퍼티를 상속받는데, 심지어 이는 객체가 생성된 이후에 프로토타입이 변경되더라도 마찬가지다. 다시말해 자바스크립트 객체의 프로토타입에 메서드를 추가함으로써 간단히 자바스크립트 클래스를 확장할 수 있다는 뜻이다.
Object.prototype에도 메서드를 추가할 수 있고, 그러면 모든 객체에서 추가된 메서드를 사용할 수 있지만 이 프로퍼티는 모든 for/in 루프에서 열거될 것이기 때문에 권장하지 않는다.
호스트 환경(웹브라우저 같은)에서 정의된 클래스를 이러한 방식으로 확장할 수 있는지는 호스트 환경의 구현체마다 다르다. 이 때문에 클라이언트 측 프로그래밍에서 이러한 기법의 사용은 몹시 제한받는다.


9.5 클래스와 자료형

9.5.1 instanceof 연산자

왼쪽 피연산자는 클래스를 판별하려는 객체이며, 오른쪽 피연산자는 생성자 함수여야 하는데, 이 생성자 함수의 이름이 곧 해당 클래스의 이름이다. 표현식 o instanceof c는 만약 o가 c.prototype을 상속한다면 true다.(상속은 직접적일 필요없고, 만약 o가 c.prototype을 상속한 어떤 객체를 상속한다해도 이 표현식은 true이다.)
instanceof 연산자는 생성자 함수를 요구하지만, 실제로 instanceof 연산자는 객체가 어떤 프로토타입을 상속했는지를 검사하여 객체를 생성하는 데 어떤 생성자를 사용했는지 테스트하지는 않는다. 만약 생성자 함수를 검사의 기준으로 삼고 싶다면, isPrototype() 메서드를 대신 사용할 수 있다.

9.5.2 constructor 프로퍼티

어떤 객체의 클래스를 구별하는 또 다른 방법은 constructor 프로퍼티를 사용하는 것이다. constructor 프로퍼티를 사용하는 이 기법은 instanceof와 같은 문제가 있으며 자바스크립트 객체 가운데는 constructor 프로퍼티가 없는 것도 있을 수 있다.


9.6 자바스크립트의 객체 지향 기법

9.6.3 표준 변환 메서드

객체 자료형을 변환하는 데 사용되는 중요한 메서드들이 있고, 이 중 몇 가지 형 변환이 푤아할 때 자바스크립트 인터프리터에 의해 자동으로 호출된다. 작성한 클래스에 변환 메서드를 구현하지 않았다면, 이는 단순히 술수로 구현하지 않은 것이 아니라 의도적인 것이어야 한다.
먼저, 가장 중요한 메서드는 toString()이다. 이 메서드의 목적은 객체의 문자열 표현을 반환하는 것이다. 자바스크립트는 필요할 때 자동으로 이 메서드를 호출한다. 프로퍼티 이름 같이 문자열을 요구하는 곳에 객체를 사용했을 때 또는 문자열 결합을 위해 + 연산자를 사용했을 때 toString() 메서드가 호출된다. 만약 이 메서드를 구현하지 않으면, 클래스는 Object.prototype의 기본 구현을 상속할 것이고, 그 기본 구현 메서드는 쓸모 없는 문자열 “[object Object]”를 반환할 것이다.
다음 메서드는 valueOf()이다. 이 메서드는 객체를 원시 값으로 변환한다. 예를 들면, valueOf() 메서드는 객체가 숫자 컨텍스트에서 산술 연산자(+ 제외)나 비교 연산자와 함께 사용될 때 자동으로 호출된다. 객체 대부분은 원시 값으로 변환할 필요가 없으므로, 이 메서드를 정의하지 않는다.
다음 메서드는 toJson()이고 JSON.stringify()에 의해 자동으로 호출된다. JSON 형식은 데이터 구조를 직렬화하는데 사용되고 자바스크립트 원시 값, 배열, 일반 객체를 처리할 수 있다. 직렬화 할 때 객체의 프로토타입과 생성자는 무시된다.

9.6.4 비교 메서드

자바스크립트의 동치 연산자들은 객체를 비교할 때, 값이 아니라 참조를 사용한다. 즉, 주어진 두 참조가 같은 객체를 가리키고 있는지를 본다. 만약 어떤 클래스를 정의하고 이 클래스의 인스턴스를 비교하려면 비교하는 데 사용할 메서드를 정의해야 한다.
인스턴스를 동등 비교하려면, equals()라는 인스턴스 메서드를 정의해야 한다. equals() 메서드는 하나의 인자를 받고, 전달받은 인자와 equals 메서드를 가진 객체가 같다면 true를 반환한다. 클래스 컨텍스트상에서 ‘같다’라는 것이 어떤 의미인지는 구현에 달려있다.
객체를 어떤 순서에 따라 비교하는 것도 유용하다. 어떤 클래스의 한 인스턴스가 다른 인스턴스보다 ‘작다’ 또는 ‘크다’라고 말할 수 있게된다. 만약 <와 <=같은 관계 연산자에 객체를 사용하면 자바스크립트는 먼저 해당 객체의 valueOf() 메서드를 호출하고, 이 메서드가 원시 값을 반환하면 값을 비교한다. 그러나 대다수 클래스에는 valueOf() 메서드가 없다. 만약 valueOf() 메서드가 없는 객체들을 명시적으로 선택한 순서에 따라 비교하려면, compareTo() 메서드를 정의해야 한다.
compareTo() 메서드는 하나의 인자를 받고, 메서드 호출 대상 객체와 인자를 비교한다. 만약 this 객체가 인자 객체보다 작으면 compareTo()는 0보다 작은 값을 반환해야 하며, this 객체가 인자 객체보다 크면 0보다 큰 값을 반환해야 한다. 만약 두 객체가 같으면 을 반환해야 한다. 가장 좋은 방법은 equals() 메서드와 compareTo() 메서드를 일관성 있게 작성하는 것이다.
클래스에 compareTo() 메서드를 정의하는 한 가지 이유는 해당 클래스의 인스턴스들로 구성된 배열을 정렬하기 위함이다. Array.sort() 메서드는 추가 인자로 인스턴스를 비교하는 함수를 받는데, 이 비교 함수는 compareTo() 메서드와 똑같은 반환 값 규칙을 사용한다.

1
ranges.sort(function(a, b) { return a.compareTo(b); });

9.6.6 private 상태

인스턴스를 생성할 때, 생성자 호출의 클로저에 포착된 변수(혹은 인자)를 사용하면 private 인스턴스 필드를 흉내 낼 수 있다. 그러면 생성자 내부에서 함수들을 정의하고, 이 함수들을 새로 생성한 객체의 프로퍼티로 할당해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 예제) 시작점과 끝점에 대해 약한 캡슐화가 적용된 Range 클래스
function Range(from , to) {
// this 객체의 프로퍼티로 from, to를 저장하지 말 것.
// 대신에 시작점과 끝점을 반환하는 접근자 함수를 정의한다.
// 인자로 넘어온 from, to 값은 클로저에 저장된다.
this.from = function() { return from; }
this.to = function() { return to; }

// 프로토타입의 메서드들은 생성자에 인자로 전달된 from, to를 직접 볼 수 없다.
// 프로토타입의 메서드들은 다른 모든 것과 마찬가지로 접근자 메서드를 호출해야 한다.
Range.prototype = {
constructor: Range,
includes: function(x) { return this.from() <= x && x <= this.to(); };
}
};

이러한 캡슐화 기법에는 오버헤드가 있다. 상태를 캡슐화하도록 클로저를 사용하는 클래스는 그렇지 않은 클래스보다 확실히 느리고 크다.


9.7 서브클래스

객체 지향 프로그래밍에서 클래스 B는 다른 클래스 A를 확장(extend)하거나 클래스 A의 하위클래스가 될 수 있다. 이런 경우, 클래스 A를 슈퍼클래스라 하고 클래스 B를 서브클래스라고 한다. 클래스 B의 인스턴스는 클래스 A의 모든 인스턴스 메서드를 상속한다. 클래스 B의 메서드가 클래스 A의 메서드를 재정의했을 때, 클래스 B의 재정의된 메서드에서 클래스 A의 원래 메서드를 호출할 수가 있는데 이를 메서드 체이닝이라고 한다. 비슷하게 서브클래스의 생성자 B()가 슈퍼클래스의 생성자 A()를 호출할 필요가 있는데, 이는 생성자 체이닝이라고 한다.

이하 내용은 추후에 업데이트 예정… 다른 부분보다 확실히 이해가 많이 필요한 부분이라 몇 번 더 읽어봐야 할 것 같다.



출처 : “JavaScript: The Definitive Guide, by David Flanagan (O’Reilly). Copyright 2011 David Flanagan, 978-0-596-80552-4”

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×