정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.
자바스크립트는 프로토타입 기반 언어 → "상속"이라는 개념 없음
클래스기반의 타언어 개발자들에게 혼란을 주었고, 클래스와 비슷한 여러 기법들이 있긴했다. 결국 ES6에 클래스 문법이 추가되었지만, 일정부분 프로토타입을 활용하고 있기 때문에 ES5 체제 하에서 클래스를 흉내내기 위한 구현방식을 알고 있어야 한다.
클래스: 공통 요소를 지니는 집단을 분류하기 위한 개념 (추상적 혹은 구체적)**
- 음식, 과일 같은 "범주"
: 어떤 사물들의 공통 속성을 모아 정의한 것일 뿐 직접 만질 수도 볼 수도 없는 추상적인 개념
인스턴스: 어떤 클래스의 속성을 지니는 실존하는 개체 (구체적)
- 범주 아래의 배,사과,바나나 등
: 직접 만질 수 있고 볼 수도 있고 먹을 수 있는 구체적이고 실존하는 사물
음식은 과일보다 상위의 개념(상위클래스)이고, 과일은 음식보다 하위의 개념(하위클래스)이다.
💡과일 분류 하위에 또 다른 분류가 있으면, 클래스 간의 관계는 어떻게 될까?
클래스 간의 상하관계
현실세계
프로그래밍 언어
예제 1 ) static method, prototype method
//생성자
var Rectangle = function(width, height){
this.width = width;
this.height = height;
};
//(프로토타입)메서드
Rectangle.prototype.getArea = function(){
return this.width * this.height;
};
//스태틱 메서드
Rectangle.isReactangle = function (instance) {
return instance instanceof Rectangle &&
instance.width > 0 && instance.height > 0;
};
var rect1 = new Rectangle(3,4)
console.log(rect1.getArea()); //12(o)
console.log(rect1.isRectangle(rect1)); //Error(x)
console.log(Rectangle.isRectangle(rect1)); //true
🔹프로토타입 메서드
프로토타입 객체에 할당한 메서드는 인스턴스가 마치 자신의 것처럼 호출할 수 있다. 따라서 rect1.getArea()
은 Rectangle.prototype.getArea
을 실행시킨다. 이처럼 인스턴스에서 직접 호출할 수 있는 메서드가 프로토타입 메서드이다.
🔹스태틱 메서드
이와 다르게 rect1.isRectangle(rect1)
은 접근이 불가능하여 에러를 발생시킨다. 이처럼 인스턴스에서 직접 접근할 수 없는 메서드가 스태틱 메서드이다. 스태틱 메서드는 Rectangle.isRectangle(rect1)
처럼 생성자 함수를 this로 지정해야만 호출할 수 있다.
"프로그래밍 언어에서의 클래스는 사용하기에 따라 추상적일 수도 있고 구체적인 개체가 될 수 도 있다"
🙄뭔 말이여...?🙄
일반적인 사용 방식, 즉 구체적인 인스턴스가 사용할 메서드를 정의한 '틀'의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만, 클래스 자체를 this로 해서 직접 접근해야만 하는 스태틱 메서드를 호출할 때의 클래스는 그 자체가 하나의 개체로서 취급된다.
상속은 객체지향의 꽃!
ES5까지 JS커뮤니티에서 클래스 상속을 다른 객체지향언어에 익순한 개발자들에게 친숙한 형태로 흉내 내는 것이 주요한 관심사였다. 이번 챕터에서는 프로토타입체인을 활용해 클래스 상속을 구현하고 최대한 전통적인 객체지향 언어에서의 클래스와 비슷한 형태로까지 발전시켜보는 것이 목표이다. 플러스, 이런 다양한 방식으로 고군분투해왔구나 정도로 읽어보자! 후에 ES6의 클래스 문법도 공부할거니까!
🤩프로토타입에서 다룬 프로토타입체인이 상속의 핵심이다
자바스크립트에서 클래스 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 연결한 것!!
예제 2 )
var Grade = function(){
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};
Grade.prototype=[]; //g.__proto__ 가 빈 배열을 가리키고 있다.
var g = new Grade(100, 80);
두가지 문제점이 있다
1)length 프로퍼티가 삭제가능(configurable)하다
2) Grade.prototype에 "빈 배열"을 참조 시켰다
...
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프로퍼티는 configurable 속성이 false라서 삭제가 불가능
하지만!! Grade 클래스의 인스턴스는 배열 메서드를 상속하지만 기본적으로는 일반 객체의 성질을 그대로 지니므로 삭제가 가능해서 문제이다!
잠깐... push했을 때, 어떻게 0번째 인덱스에 70이 들어가고 length가 다시 1이 되었을까?
g.__proto__
,즉 Grade.prototype이 빈 배열을 가리키고 있기 때문에 가능했다.
push 명령에 따라 JS엔진은 g.length를 읽으려는데 없으니까 프로토타입 체이닝을 타고 g.__proto__.length
를 읽어온 것이다. 빈 배열의 length는 0이므로 여기에 값을 할당하고, length는 1만큼 증가시키라는 명령이 문제 없이 작동했다.
그럼 만약 Grade.prototype에 "요소를 포함하는 배열을 매칭"시켰다면?
Grade.prototype = ['a','b','c','d'];
var g = new Grade(100, 80);
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:100, 1:80, 2:90, ___4:70, length: 5}
length삭제 전까지는 동일하나, length를 삭제하고 나니 빈배열을 매칭했을때와 다르게 동작했다.
g.length가 없으니까 g.__proto__.length
를 찾고, 값이 4이므로 인덱스 4에 70을 넣고, 다시 g.length에 5를 부여하는 순서로 동작한 것이다.
이렇게 클래스에 있는 값이 인스턴스의 동작에 영향을 주어서는 안될텐데...🤔
인스턴스와의 관계에서는 구체적인 데이터를 지니지 않고 오직 인스턴스가 사용할 메서드만을 지니는 추상적인 "틀"로서만 작용하도록 작성해야 위의 예제같은 예기치 않은 오류를 없앨 수 있다.
(ES5까지의 클래스 상속구현은 문제가 발생할 여지가 있었다. 그래서 ES6에서 클래스 문법이 도입되었다.)
근데... 클래스의 추상성을 지키는게 왜 중요할까?!
상위 클래스의 데이터가 오염되면, 상위클래스의 공통성질을 참조하고 있는 다른 수많은 하위클래스까지 데이터 오염이 일어나기 때문이다.
추상성
추상화
[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
JavsScript - Classes | MDN
https://velog.io/@jeanbaek/07-클래스