Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions 7_클래스/남현준.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# 7장 클래스

- 자바스크립트는, 프로토타입 기반 언어
- 따라서 상속의 개념이 존재하지 않았다!
- ES6 이후로는, 클래스 문법이 추가되었음
- 다만 클래스에서도 일정 부분은 프로토타입을 활용한다.



## 클래스, 그리고 인스턴스

- 어떠한 범주 안에는, 다양한 하위 분류들이 있다.

- 음식이라는 커다란 범주 안에는 고기, 과일등의 하위 분류가 있다.

- 고기라는 하위 분류 안에는 또 다시 돼지, 소, 닭 등의 하위 분류가 있다.

- 돼지고기라는 하위 분류 안에는 다시 안심, 등심, 앞다리 등의 분류가 있다..

![class image](https://user-images.githubusercontent.com/62874043/265658381-0e535e33-3c16-42e5-b32b-97447eda488b.png)

- 돼지고기라는 하위 분류는 실존하는 개념이지만, 음식, 고기라는 상위 분류는 추상적인 개념이다!

- 이를 통해, 음식이나 고기와 같은 상위 분류는 모두 집단, 다시 말해 **클래스**이다.
- **하위 개념은 상위 개념을 포함하면서, 더 구체적인 개념을 가진다.**
- 클래스는 하위 개념으로 내려갈수록, **상위 클래스의 속성을 상속하며 더 구체적인 요건(혹은 자신만의 특징)을 가지게 된다!**
- 어떤 클래스의 속성을 가지는 **실존하는 개체를 인스턴스라고 부른다.**
- 현실에서는 개체가 이미 실존하는 상태에서 이를 구분짓기 위해 클래스의 개념을 사용한다.
- 그러나 컴퓨터는 이러한 구분을 알지 못하기에, **사용자가 직접 여러 클래스를 정의해야 한다!**
- 그를 바탕으로 인스턴스를 만들 때 비로소 만들어진 개체가 특정 클래스의 속성을 가지게 된다.
- 현실과 마찬가지로 `공통 요소를 지니는 집단을 분류하기 위한 개념`으로 클래스를 정의한다는 부분에선 유사한 점이 있지만, **이미 존재하는 인스턴스들의 공통점을 찾아 클래스를 정의하는 현실**과 다르게, 프로그래밍 언어상에서는 **클래스가 먼저 정의되어야만 그로부터 공통적인 요소를 지니는 인스턴스를 생성할 수 있다**는 차이점이 있는 것!
- **한 인스턴스는, 하나의 클래스만을 바탕으로 만들어진다.**
- 다중상속의 지원 유무와 무관하게, 인스턴스 생성시 호출 가능한 클래스는 오직 하나이다!



## 자바스크립트에서의 클래스

- 자바스크립트는 프로토타입 기반 언어이지만..
- 클래스라는 관점에서 접근할 수 있는 부분도 있다.
- 생성자 함수 X를 new 키워드와 함께 호출해 인스턴스를 생성하는 프로토타입에서,
- X를 일종의 클래스라고 가정했을 때, 그를 통해 만들어지는 인스턴스에는 X의 prototype 객체 내부 요소들이 인스턴스에 상속된다 볼 수 있다.
- 실제로는 상속보다는 **프로토타입 체이닝**을 통해 `__proto__` 객체가 생성자 함수의 prototype 객체를 참조하고 있는 것이지만, 클래스를 통한 상속과 유사하게 동작한다고 보아도 무방하다.
- 생성자 함수 X 내부의 프로퍼티 중 prototype 객체를 제외한 나머지는 인스턴스에 상속되지 않는다!



### 스태틱 멤버, 인스턴스 멤버

- 이들을 나누는 기준은 **인스턴스에 상속되는지(인스턴스가 참조하는지) 여부이다!

```javascript
// 생성자 함수
const Square = function (width, height) {
this.width = width;
this.height = height;
};

// 프로토타입(인스턴스) 메서드
// 생성자 함수의 prototype 객체에 정의된 메서드이지만,
// 인스턴스의 __proto__객체에 의해 참조되기 때문에, 인스턴스에 상속(참조)되는 메서드이다!
Square.prototype.getArea = function () {
return this.width * this.height;
};

// 스태틱 메서드
// 인스턴스에서는 해당 메서드에 접근할 수 없다.
Square.isSquare = function (instance) {
return instance instanceof Square && instance.width === instance.height;
};

const sq1 = new Square(5, 5);

console.log(sq1.getArea()); // 25
console.log(sq1.isSquare(sq1)); // 인스턴스에서 접근 시도하면, TypeError 발생
console.log(Square.isSquare(sq1)); // true
```

- 인스턴스에서 스태틱 메서드 isSquare에 접근할 수 없는 이유는 무엇일까?
- 우선, 인스턴스에서 스태틱 메서드 isSquare에 접근하기 위해 인스턴스 sq1 내에서 해당 메서드를 찾는다.
- 찾지 못한 경우, 프로토타입 체이닝 과정을 따라 `sq1.__proto__`에서 다시 해당 메서드를 찾는다.
- 그럼에도 찾지 못한 경우, 프로토타입 체인을 거슬러 올라 마지막으로 `Object.prototype`에 까지 접근해서 해당 메서드를 찾는다.
- 여기에서도 찾지 못한다면, `Uncaught TypeError: ~~ is not a function` 과 같은 오류가 발생한다.
- 이처럼 인스턴스에서 직접 접근할 수 없는 메서드를 **스태틱 메서드**라고 한다!
- 이 경우, 클래스는 추상적인 개념이 아닌 this에 바인딩되는 하나의 개체가 된다.



## 클래스 상속

- 클래스 개념이 본격적으로 등장하기 전인 ES5까지는...

- 클래스 상속을 구현했다는 말은,
- 기본적으로 **잘 연결된 프로토타입 체이닝**을 의미했다.
- 하지만, 상위 클래스와 하위 클래스간의 관계를 완벽히 구현한 것은 아니다!

```javascript
const Students = function () {
const args = Array.prototype.slice.call(arguments);
for (let i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};

Students.prototype = [];
const g = new Students("김철수", "홍길동");

console.log(g); // Array { '0': '김철수', '1': '홍길동', length: 2 }

delete g.length;
console.log(g); // Array { '0': '김철수', '1': '홍길동' }

g.push("이순신");
console.log(g); // Array { '0': '이순신', '1': '홍길동', length: 1 }
```

- length 프로퍼티를 삭제하고, 새로운 값을 배열에 넣었더니 0번째 인덱스의 값이 변경되었다!
- 생성자 함수 Students의 prototype이 빈 배열을 참조하는 것이 원인이다.
- 인스턴스 g에서 length 프로퍼티를 삭제한 이후, 배열에 요소를 추가할 때 JS 엔진이 g.length를 읽고자 하는데, g에는 length 프로퍼티가 없기에(delete를 통해 제거했기에) 프로토타입 체이닝을 통해 Students.prototype.length(`g.__proto__.length`)를 읽어온 것이다.
- 생성자 함수의 prototype은 빈 배열이기 때문에, 여기에 새롭게 값을 추가하고, 프로토타입에서의 length를 1 추가시킨 것이다.
- 만약 생성자 함수의 prototype이 n개의 요소를 가지고 있는 배열을 가리키는 상황이었다면, 위의 예시에서 `g.push("이순신")` 명령이 수행되면, 이순신이라는 값은 배열의 n번째 인덱스에 들어가고, 배열의 length 값은 n + 1이 된다.
- 만약 프로토타입 체이닝을 통한 상속의 구현이 일반적인 부모-자식 관계를 완벽히 구현했다면, 생성자 함수 Students의 특징을 인스턴스 g에서도 동일하게 가지고 있어야 한다.
- 이처럼, 클래스가 구체적인 데이터를 가져버리고, 클래스에 있는 값이 인스턴스의 동작에 영향을 주는 경우가 발생하면, 클래스의 추상성을 해치게 된다.



### 클래스가(프로토타입이) 구체적인 데이터를 지니지 않게 하려면?

- 가장 쉬운 방법은 인스턴스 생성 이후 새로운 프로퍼티를 추가할 수 없게 하는 것!

- 인스턴스가 생성되면, 생성자 함수의 prototype에 정의된 프로퍼티를 삭제한다.
- prototype에 정의된 프로퍼티가 많다면, 삭제하는 과정이 비효율적일 수 있다.
- 그 후, `Object.freeze()` 메서드를 통해 객체를 동결시켜 더 이상 변경될 수 없게(새로운 프로퍼티를 추가할 수 없게) 만든다!
- 동결된 객체는 새로운 속성을 추가하거나, 존재하는 속성을 제거하는 것을 방지한다.
- 동결된 객체는 그 프로토타입이 변경되는 것도 방지한다.

- 다음 방법은 하위 클래스의 prototype에 직접 상위 클래스의 인스턴스를 할당하는 대신, 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수를 하나 더 만들어 그 prototype이 상위 클래스의 prototype을 바라보게 한 다음, 하위 클래스의 prototype에는 빈 생성자 함수의 인스턴스를 할당하는 것이다!

```javascript
const Rectangle = function (width, height) {
this.width = width;
this.height = height;
};

Rectangle.prototype.getArea = function () {
return this.width * this.height;
};

const Square = function (width) {
Rectangle.call(this, width, width);
};

const Bridge = function () {};

Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

console.log(Bridge.prototype); // { getArea: [Function (anonymous)] }
console.log(Square.prototype); // Rectangle {}
```

- Bridge라는 빈 생성자 함수를 만든다.
- Bridge.prototype이 Rectangle.prototype을 참조하게 한다.
- 마지막으로 Square.prototype에 Bridge의 인스턴스를 할당한다.
- 그 후 Square.prototype을 동일하게 freeze() 메서드로 동결한다!
- 객체가 동결되었기 때문에, 이제 Square.prototype에 프로퍼티를 생성하거나, 삭제하려는 시도는 무시된다.

```javascript
Bridge.prototype = Rectangle.prototype;
Square.prototype = new Bridge();
Object.freeze(Square.prototype);

// 새로운 프로퍼티 생성 시도
Square.prototype.introduce = function () {
return "hello";
};
// 기존 프로퍼티 삭제 시도
delete Square.prototype;

console.log(Square.prototype); // Rectangle {}
```



## ES6의 클래스, 그리고 클래스 상속

- ES6에 들어서, 클래스 문법이 도입되었음!

- 클래스 내 메서드와 메서드 사이에는, 콤마로 구분하지 않는다.

```javascript
const cl = class {
// `constructor`라는 이름에서 알 수 있듯, 생성자 함수의 역할을 수행한다.
constructor(name) {
this.name = name;
}
// static 키워드를 통해, 해당 메서드가 스태틱 메서드임을 알림
// ES6 이전 프로토타입에서는, 생성자 함수 안에 메서드를 정의해 인스턴스에서 직접 호출할 수 없었던 것
static staticMethod() {
return this.name + " is static method";
}
// prototype 객체 내부에 할당되는 메서드
// ES6 이전 프로토타입에서는, prototype에 메서드를 정의해 인스턴스에서 프로토타입 체이닝을 통해 접근할 수 있었던 것
method() {
return this.name + " is method";
}
};

const clInstance = new cl("pizza");
console.log(cl.staticMethod()); // cl is static method
console.log(clInstance.method()); // pizza is method
```

- `super()`키워드를 통해, 상위 클래스의 생성자 함수를 호출할 수 있다.

```javascript
const Rectangle = class {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
};

// extends 키워드를 통해 상속 관계를 나타낸다.
const Square = class extends Rectangle {
constructor(width) {
super(width, width); // 상위 클래스의 생성자 함수를 실행한다!
}
getArea() {
console.log(this) // Square { width: 3, height: 3 }
console.log("size is :", super.getArea());
}
};
```