스터디
[JavaScript] 프로토타입과 클래스 본문
< 객체 생성자 >
프로토타입과 클래스에 대해서 알아보기 전에 우선 객체 생성자라는 것을 알아보자.
객체 생성자는 함수를 통해서 새로운 객체를 만들고 그 안에 넣고 싶은 값 혹은 함수들을 구현할 수 있게 해준다.
function Animal(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
this.say = function() {
console.log(this.sound);
};
}
const dog = new Animal('개', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');
dog.say(); // 멍멍
cat.say(); // 야옹
- 객체 생성자를 사용할 때는 보통 함수의 이름을 대문자로 시작하고, 새로운 객체를 만들 때는 new 키워드를 앞에 붙여주어야 한다.
- dog가 가지고 있는 say 함수와 cat이 가지고 있는 수행하는 코드가 똑같음에도 불구하고 객체가 생성될 때마다 함수도 새로 만들어져서 this.say로 설정이 되고 있다.
- 같은 객체 생성자 함수를 사용하는 경우, 특정 함수 또는 값을 재사용할 수 있는 것이 바로 프로토타입이다.
< 프로토타입 >
- JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 불린다.
- 모든 객체들이 메소드와 속성들을 상속받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미이다.
- 프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속받을 수 있고 그 상위 프로토타입 객체도 마찬가지이다. -> 프로토타입 체인이라 부르며 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 근간이다.
- 정확히 말하자면 상속되는 속성과 메소드들은 각 객체가 아니라 객체의 생성자의 prototype이라는 속성에 정의되어있다.
- 프로토타입은 객체 생성자 함수 아래에 '.prototype.[원하는 키] =' 코드를 입력하여 설정할 수 있다.
function Animal(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
}
Animal.prototype.say = function() {
console.log(this.sound);
};
const dog = new Animal('개', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');
dog.say(); // 멍멍
cat.say(); // 야옹
console.log(dog.sharedValue); // 1
console.log(cat.sharedValue); // 1
- dog 객체와 cat 객체의 멤버인 __proto__(비표준) 속성이 프로토타입 객체를 가리키는 숨은 링크가 프로토타입이라고 한다.
- prototype 속성은 프로토타입 객체를 참조하는 속성이다.
- 그리고 함수와 new 연산자가 만나 생성한 객체의 프로토타입 객체를 지정해주는 역할을 한다.
- 객체 안의 __proto__ 속성은 자신을 만들어낸 원형인 프로토타입 객체를 참조하는 숨겨진 링크로써 프로토타입을 의미한다.
JavaScript에서는 숨겨진 링크가 있어 프로토타입 객체 멤버에 접근할 수 있다.
그래서 이 프로토타입 링크를 사용자가 정의한 객체에 참조되도록 설정하면 코드의 재사용과 객체지향적인 프로그래밍을 할 수 있다.
< 프로토타입의 코드 재사용 >
JavaScript는 프로토타입 기반 언어이기 때문에 프로토타입을 이용하여 코드 재사용을 할 수 있다.
1) 기본 방법
- 부모에 해당하는 함수를 이용하여 객체를 생성한다
- 자식에 해당하는 함수의 prototype 속성을 부모 함수를 이용하여 생성한 객체를 참조하는 방법이다.
function Person(name) {
this.name = name || '철수';
}
Person.prototype.getName = function() {
return this.name;
};
function Korean(name) {}
Korean.prototype = new Person();
const kor1 = new Korean();
console.log(kor1.getName()); // 철수
const kor2 = new Korean('영희');
console.log(kor2.getName()); // 철수
- 부모에 해당하는 함수는 Person이다.
- 자식 함수인 Korean 함수 안의 prototyupe 속성을 부모 함수로 생성된 객체로 바꿨다.
- 이제 Korean 함수와 new 연산자를 이용하여 생성된 kor 객체의 __proto__ 속성이 부모 함수를 이용하여 생성된 객체를 참조한다.
- 이 객체가 Korean 함수를 이용하여 생성된 모든 객체의 프로토타입 객체가 된다.
- kor1에는 name과 getName()이라는 속성이 없지만, 부모에 해당하는 프로토타입 객체에 name이 있다.
- 이 프로토타입 객체의 부모에 getName()을 가지고 있어 kor1에서 사용할 수 있다.
- 이 방법에도 단점이 있는데, 부모 객체의 속성과 부모 객체의 프로토타입 속성을 모두 물려받게 된다.
- 대부분의 경우 객체 자신의 속성은 특정 인스턴스에 한정되어 재사용할 수 없어 필요가 없다.
- 또한 자식 객체를 생성할 때 인자를 넘겨도 부모 객체를 생성할 때 인자를 넘겨주지 못한다.
- kor2 객체를 생성할 때 Korean 함수의 인자로 '영희'라고 주었다.
- 객체를 생성한 후 getName()을 호출하면 '영희'라고 출력될 것 같지만, 부모 생성자에게 인자를 넘겨주지 않았기 때문에 name에는 default 값인 '철수'가 들어있다.
- 객체를 생성할 때마다 부모의 함수를 호출할 수도 있지만 매우 비효율 적이다.
-> 기본 방법의 문제점을 해결하는 방법: 생성자 빌려 쓰기
2) 생성자 빌려 쓰기
- 기본 방법의 문제점인 자식 함수에서 받은 인자를 부모 함수로 인자를 전달하지 못했던 부분을 해결한다.
- 부모 함수의 this에 자식 객체를 바인딩하는 방식이다.
function Person(name) {
this.name = name || '철수';
}
Person.prototype.getName = function() {
return this.name;
};
function Korean(name) {
Person.apply(this, arguments);
}
const kor1 = new Korean('영희');
console.log(kor1.name); // 영희
- 함수 내부에서 apply 함수를 이용한다.
- 부모 객체인 Person 함수 영역의 this를 Korean 함수 안의 this로 바인딩한다.
- 이것은 부모의 속성을 자식 함수 안에 모두 복사한다.
- 객체를 생성하고 name을 출력한다.
- 객체를 생성할 때 넘겨준 인자를 출력하는 것을 볼 수 있다.
- 기본 방법에서는 부모 객체의 멤버를 참조를 통해 물려받았다.
- 하지만 생성자 빌려 쓰기는 부모 객체 멤버를 복사하여 자신의 것으로 만들어버린다는 차이점이 있다.
- 하지만 이 방법은 부모 객체의 this로 된 멤버들만 물려받게 되는 단점이 있다.
- 그래서 부모 객체의 프로토타입 객체의 멤버들을 물려받지 못한다.
- kor1 객체에서 부모 객체의 프로토타입 객체에 대한 링크가 없다는 것을 볼 수 있다.
3) 생성자 빌려 쓰고 프로토타입 지정해주기
- 이 방법은 방법1과 방법2 문제점들을 보완하면서 JAVA에서 예상할 수 있는 동작 방식과 유사하다.
function Person(name) {
this.name = name || '철수';
}
Person.prototype.getName = function() {
return this.name;
};
function Korean(name) {
Person.apply(this, arguments);
}
Korean.prototype = new Person();
const kor1 = new Korean('영희');
console.log(kor1.getName()); // 영희
- 부모 함수 this를 자식 함수 this로 바인딩한다.
- 자식 함수 prototype 속성을 부모 함수를 사용하여 생성된 객체로 지정했다.
- 부모 객체 속성에 대한 참조를 가지는 것이 아닌 복사본을 통해 내 것으로 만든다.
- 동시에 부모 객체의 프로토타입 객체에 대한 링크도 참조된다.
- 부모 객체의 프로토타입 객체 멤버도 사용할 수 있다.
- 이 방법에도 문제점이 있는데, 부모 생성자를 두 번 호출한다.
- 생성자 빌려 쓰기 방법과 달리 getName()은 제대로 상속되었지만, name에 대해서는 kor1 객체와 부모 함수를 이용하여 생성한 객체에도 있는 것을 볼 수 있다.
4) 프로토타입 공유
- 부모 생성자를 한 번도 호출하지 않으면서 프로토타입 객체를 공유하는 방법이다.
function Person(name) {
this.name = name || '혁준';
}
Person.prototype.getName = function() {
return this.name;
};
function Korean(name) {
this.name = name;
}
Korean.prototype = Person.prototype;
const kor1 = new Korean('영희');
console.log(kor1.getName()); // 영희
- 자식 함수의 prototype 속성을 부모 함수의 prototype 속성이 참조하는 객체로 설정했다.
- 자식 함수를 통해 생성된 객체는 부모 함수를 통해 생성된 객체를 거치지 않고, 부모 함수의 프로토타입 객체를 부모로 지정하여 객체를 생성한다.
- 부모 함수의 내용을 상속받지 못하므로 상속받으려는 부분을 부모 함수의 프로토타입 객체에 작성해야 사용자가 원하는 결과를 얻게 된다.
5) prototypal한 방식의 재사용
- 이 방법은 Object.create()를 사용하여 객체를 생성과 동시에 프로토타입 객체를 지정한다.
- 이 함수는 첫 번째 매개변수는 부모 객체로 사용할 객체를 넘겨주고, 두 번째 매개변수는 선택적 매개변수로써 반환되는 자식 객체의 속성에 추가되는 부분이다.
- 이 함수를 사용함으로써 객체 생성과 동시에 부모 객체를 지정하여 코드의 재활용을 간단하게 구현할 수 있다.
const person = {
type: '인간',
getType: function() {
return this.type;
},
getName: function() {
return this.name;
}
};
const cs = Object.create(person);
cs.name = '철수';
console.log(cs.getType()); // 인간
console.log(cs.getName()); // 철수
- 부모 객체에 해당하는 person을 객체 리터럴 방식으로 생성했다.
- 그리고 자식 객체 cs는 Object.create() 함수를 이용해 첫 번째 매개변수로 person을 넘겨받아 cs 객체를 생성했다.
- 한 줄로 객체를 생성함과 동시에 부모 객체의 속성도 모두 물려받았다.
- 위의 1~4번에 해당하는 classical 방식보다 간단하면서 여러 가지 상황을 생각할 필요도 없다.
- JavaScript에서는 new 연산자와 함수를 통해 생성한 객체를 사용하는 classical 방식보다 prototypal 방식을 더 선호한다.
< 클래스 >
클래스라는 기능은 다른 프로그래밍 언어네는 있는 기능인데 자바스크립트에는 없었기 때문에 예전 자바스크립트(ES5)에서는 클래스 문법이 따로 없었기 때문에 객체 생성자 함수를 사용하여 비슷한 작업을 구현해왔다.
ES6에서부터는 class라는 문법이 추가되었는데, 우리가 객체 생성자로 구현했던 코드를 조금 더 명확하고 깔끔하게 구현할 수 있게 해주며 상속도 훨씬 쉽게 해줄 수 있다.
class Animal {
constructor(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
}
say() {
console.log(this.sound);
}
}
const dog = new Animal('개', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');
dog.say(); // 멍멍
cat.say(); // 야옹
여기서 say라는 함수를 클래스 내부에 선언했는데, 클래스 내부의 함수를 메소드라고 부른다.
이렇게 메소드를 만들면 자동으로 prototype으로 등록이 된다.
class를 사용했을 때는 다른 클래스를 쉽게 상속할 수 있다.
class Animal {
constructor(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
}
say() {
console.log(this.sound);
}
}
class Dog extends Animal {
constructor(name, sound) {
super('개', name, sound);
}
}
class Cat extends Animal {
constructor(name, sound) {
super('고양이', name, sound);
}
}
const dog = new Dog('멍멍이', '멍멍');
const cat = new Cat('야옹이', '야옹');
dog.say(); // 멍멍
cat.say(); // 야옹
상속할 때는 extends 키워드를 사용하며, constructor에서 사용하는 super() 함수가 상속받은 클래스의 생성자를 가리킨다.
class Animal {
constructor(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
}
say() {
console.log(this.sound);
}
}
class Dog extends Animal {
constructor(name, sound) {
super('개', name, sound);
}
}
class Cat extends Animal {
constructor(name, sound) {
super('개', name, sound);
}
}
const dog = new Dog('멍멍이', '멍멍');
const dog2 = new Dog('왈왈이', '왈왈');
const cat = new Cat('야옹이', '야옹');
const cat2 = new Cat('냐옹이', '냐옹');
dog.say(); // 멍멍
dog2.say(); // 왈왈
cat.say(); // 야옹
cat2.say(); // 냐옹
< Prototype Based- vs. Class Based- >
자바스크립트는 객체지향 언어지만 기존 클래스 기반 프로그래밍 방식을 따르지 않으며, 프로토타입 기반 프로그래밍 방식으로 동작한다. 하지만 ES6 이후로 자바스크립트 내에서도 클래스 문법이 출현하였다.
function Person(name) {
this.name = name;
}
Person.prototype.say = function() {
console.log(`Hi, My name is ${this.name}.`);
};
const gd = new Person('홍길동');
console.log(gd.say()); // Hi, My name is 홍길동.
class Person {
constructor(name) {
this.name = name;
}
say() {
console.log(`Hi, My name is ${this.name}.`);
}
}
const gd = new Person('홍길동');
console.log(gd.say()); // Hi, My name is 홍길동.
두 코드의 결과는 동일하며, 동작 방식도 거의 차이가 없다.
즉, ES6 클래스 문법은 단지 생성자-프로토타입 패턴을 숨기기 위한 Syntactic Sugar에 불과하다.
자바스크립트는 여전히 프로토타입 기반 언어이다.
클래스 기반 언어에서 클래스란 '객체의 상태와 기능을 정의한 틀'이며 생성될 객체에 대한 일종의 청사진으로 이해할 수 있다. 그리고 이것의 인스턴스화를 통해 생성된 객체를 인스턴스라고 한다. 여기서 인스턴스는 클래스에서 정의된 모든 특성에 대한 복사본(copy)이기 때문에 두 개념은 개별적이며 서로 간의 참조는 존재하지 않는다.
반면에 프로토타입 기반 언어네서는 클래스-인스턴스의 개념이 아닌 오직 객체 개념만 존재한다. ES6에 도입된 클래스 문법은 실제 클래스 패턴에 대한 모방일 뿐이다. 이것은 자바스크립트의 생성자 함수 패턴을 좀 더 클래스답게 보이기 위한 장치이며, 생성된 객체는 함수의 프로토타입 객체에 연결되어있다.
class Person {}
const gd = new Person();
console.log(gd.__proto__);
// {constructor: class Person, __proto__: Object}
프로토타입 체계에서 '상속'이라는 표현은 적절치 않을 수 있는데, 클래스 기반 언어의 관점으로 보았을 때 상속의 동작은 결국 인스턴스화와 마찬가지로 copy에 기반한 동작이기 때문이다.
자바스크립트에서 객체들이 동작하는 방식을 고려해본다면 상속보다는 오히려 링크를 통한 위임으로 바라보는 것이 더 적절한 것 같다.
https://www.nextree.co.kr/p7323/
JavaScript : 프로토타입(prototype) 이해
JavaScript는 클래스라는 개념이 없습니다. 그래서 기존의 객체를 복사하여(cloning) 새로운 객체를 생성하는 프로토타입 기반의 언어입니다. 프로토타입 기반 언어는 객체 원형인 프로토타입을 이
www.nextree.co.kr
'백엔드' 카테고리의 다른 글
[DB] Join (0) | 2023.05.20 |
---|---|
CORS (0) | 2023.04.13 |
[JavaScript] 싱글톤 패턴과 정적 클래스 (0) | 2023.04.07 |
MVC 패턴 (2) | 2023.04.07 |
[JavaScript] 동기? 비동기? (0) | 2023.04.06 |