- 프로토타입 ⏪
- 스코프
- 호이스팅
- 클로저
- 모듈
class
를 통해 직관적으로 구현할 수 있다. ⇒ 즉, 프로토타입의 원리는 이해하고 class를 통해 상속 개념을 구현하자.객체의 프로토타입은 참조 링크 형태로 [[Prototype]] 내부 프로퍼티에 저장된다. 참조 링크 형태로 저장되기에 동일한 프로토타입을 상속한 객체는 모두 같은 프로퍼티와 메소드를 공유한다.
Parent.prototype
을 상속받고 있다.**Parent.prototype**
의 정보를 [[Prototype]] 프로퍼티에 참조 링크 형태로 저장한다.[[Prototype]]은 자바스크립트 엔진 내부에서만 사용하는 숨겨진 프로퍼티지만 크롬, 파이퍼폭스와 같은 모던 브라우저에서 ‘proto’ 라는 프로퍼티로 접근할 수 있다. 이것은 표준 명세가 아니고 공식적인 방법도 아니므로 가급적 사용하지 않는 것이 좋다.
만약 [[Prototype]]에 접근하고 싶다면 표준 메소드인 Object.getPrototypeOf()를 사용하는 것이 좋다. 추후 자바스크립트 엔진에서 proto 프로퍼티를 더 이상 지원하지 않을 가능성도 있으므로 프로토타입을 찾기 위해서는 위의 메소드 사용을 권장한다.
const obj = {
name: 'javascript'
};
console.log(obj.toString()); // '[Object object]'
위의 예제의 obj 객체에서 toString()
메소드가 정상적으로 호출된다. obj 객체 내에 toString() 메소드가 없는데, 어떻게 함수 호출이 가능할까?
✍️ 이것은 프로토타입 체인
이라는 개념 때문에 가능하다.
프로토타입 체인은 상위 프로토타입과 연쇄적으로 연결된 구조를 의미한다. 프로퍼티나 메소드에 접근하기 위해 이 연결 구조를 따라 차례대로 검색하는 것을 프로토타입 체이닝이라고 한다.
📝 위 예제는 아래와 같은 순서로 동작했다.
- obj 객체의
toString()
메소드를 호출하기 위해 obj 객체의 프로퍼티나 메소드를 검색- 1번에서 메소드를 찾지 못했다면 프로토타입 체인을 통해 상위 프로토타입에서
toString()
메소드를 검색- 상위 프로토타입에서
toString()
메소드를 찾았으므로 해당 메소드를 호출
obj 객체에 name 프로퍼티 외에 ‘__proto__
’ 프로퍼티가 있음을 알 수 있다. 이것이 숨겨진 내부 [[Prototype]]
프로퍼티, 즉 프로토타입을 가리킨다. obj 객체에 toString()
메소드가 없기 때문에 프로토타입 체인을 통해 프로토타입에 있는 toString()
을 찾아 호출했다.
Object.prototype은 프로토타입 체인의 최상위 프로토타입이다. 모든 객체가 가지고 있는 프로토타입 체인의 끝은 모두 Object.prototype이다.
객체의 부모가 되는 프로토타입은 객체가 생성되는 시점에 설정된다. obj와 같이 객체 리터럴(const obj = { key : value }
)로 생성한 모든 객체는 Object.prototype
을 프로토타입으로 설정한다.
객체 리터럴이 아닌 배열과 같이 내장된 객체의 프로토타입은 독특하게 각자 자신의 프로토타입을 따로 정의하고 있다.
const arr = [];
이는 Object.prototype과는 다른 것을 알 수 있다. 배열 객체는 프로토타입으로 **Array.prototype**
이라는 고유의 객체가 설정된다. 배열의 내장 메소드(concat, filter, forEach…)들이 정의되어 있다. 배열 메소드(arr.length 등)를 호출할 수 있었던 것은 프로토타입 체인을 통해 Array.prototype의 메소드들이 검색되어 호출되었기 때문이다.
Array.prototype 내장 프로토타입 또한 최상위 프로토타입으로 Object.prototype을 가진다. 자바스크립트에는 배열 외에 랩퍼 객체, 함수, 정규식과 같은 내장 객체들이 있고, 이러한 객체들 역시 고유의 프로토타입을 가지고 있기에 다양한 메소드와 프로퍼티를 사용할 수 있다.
종점은 모두 Object.prototype이라는 것을 기억하자.
함수에는 prototype이라는 프로퍼티가 있고, 일반적 함수에는 prototype 프로퍼티를 사용할 일이 없으나 new
키워드로 만든 생성자 함수에서는 특별한 역할을 한다. 여기서 주의할 점이 있다.
✍️ 생성자 함수로 생성된 객체는 생성자 함수의 prototype 프로퍼티가 프로토타입([[Prototype]])으로 설정된다. 아래의 Tobacco 생성자 함수의 예제를 확인하자.
function Tobacco(type) {
this.type = type;
}
const tobacco = new Tobacco('ESSE');
console.log(Tobacco.prototype === tobacco.__prototype__); // **true**
✍️ tobacco 객체의 프로토타입은 Tobacco() 생성자 함수의 prototype 프로퍼티인 Tobacco.prototype
을 참조 링크로 가리키고, 이 객체는 Object.prototype
을 프로토타입으로 가리킨다. 모든 생성자 함수를 통해 생성된 객체는 위와 같이 상속을 구현한다.
🤷♂️ 이 말인 즉슨,
Tobacco()의 prototype 프로퍼티 ↔ Tobacco.prototype의 constructor() 프로퍼티는서로 참조
하고, tobacco 객체는 프로토타입 체인으로 인해 Tobacco.prototype의 cunstructor 프로퍼티로 접근이 가능하다.
위의 개발자 도구 화면에서 **Tobacco.prototype**
↔ **constructor**
프로퍼티가 Tobacco() 생성자 함수를 참조하고 있는 것을 확인할 수 있다.
객체의 부모인 프로토타입에 메소드가 프로퍼티를 추가하는 방법은 일반 객체처럼 동적으로 프로퍼티나 메소드를 추가 및 삭제하면 된다. 변경된 프로퍼티는 실시간으로 프로토타입 체인을 통한 검색에 반영된다.
function Tobacco(type) {
this.type = type;
}
Tobacco.prototype.inhale = function() {
console.log('Puff Puff');
}
const tobacco = new Tobacco('ESSE');
console.log(tobacco.inhale()); // 'Puff Puff';
❗ Tobacco.prototype에 inhale() 메소드를 추가하여 tobacco 객체에서 inhale() 메소드를 호출할 수 있게 되었다.
📌 객체 생성 이후에 프로토타입의 프로퍼티 수정은 지양해야 한다. 모든 객체가 프로토타입을 공유하므로 수정 및 삭제하게되면 혼란과 버그를 초래할 수 있다. 즉, 동적으로 객체를 수정하는 것은 위험이 동반된다.
내장 프로토타입 Array.prototype 또는 Object.prototype도 수정이 가능은 하지만, 웬만해서는 절대 수정하지 않는 것을 권장한다.
생성된 객체와 부모 프로토타입의 링크에 영향을 주지 않게끔 구현해야 해서 까다로울 수 있다. 아래 예제의 Esse() 생성자 함수는 Tobacco() 생성자 함수를 상속받는다.
// 상속 구현 with Prototype
function Tobacco() {
console.log('initialize Tobacco');
}
Tobacco.prototype.inhale = function() {
console.log('Puff Puff');
}
Tobacco.prototype.exhale = function() {
console.log('Paaaahhhh');
}
function Esse(type){
this.type = type;
}
// 상속 함수 inherit 구현
function inherit(parent, child) {
function F() {};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
}
inherit(Tobacco, Esse);
esse = new Esse('ChangeOne');
console.log(esse);
📌 코드 설명
🤷♂️ 주의할 점
const tobacco = new Tobacco()
function Esse(type) { this.type = type; }
Esse.prototype = tobacco;
tobacco.newProperty = 'new property';
console.log(Esse.prototype.newProperty); // 'new property'
이처럼 상속받을 필요 없는 부모 클래스 특정 객체의 prototype 프로퍼티까지 상속받게 된다. 이러한 문제를 방지하고자 F() 생성자 함수를 사용해서 부모 클래스의 인스턴스와 자식 클래스의 인스턴스를 독립적으로 만들어 사용한다.생성자 빌려쓰기
위의 예제 코드에는 ‘자식 클래스의 인스턴스를 생성할 때 부모 클래스(Tobacco()) 생성자 함수가 호출되지 않는다. ’는 문제점이 있다. 예제에서 console.log(initialize Tobacco);
코드가 실행되지 않는다는 뜻이다. 이 문제는 자식 생성자 함수에서 apply()
메소드를 사용하여 해결한다.
function Esse(type) {
Tobacco.apply(this, arguments);
this.type = type;
}
Obejct.create()
메소드Object.setPrototypeOf()
메소드
위 메소드들은 굳이 inherit() 상속 함수를 직접 구현할 필요 없이 더 쉽게 객체의 프로토타입을 지정할 수 있게 도와준다.
꼭 따로 살펴보도록 하자.
프로토타입을 통한 클래스와 상속의 구현은 직관적이지 않고 번거롭다. 이러한 문제를 해결하기 위해 ES6부터 class 키워드라는 새로운 syntatic sugar가 등장했다. class 키워드를 통해 좀 더 쉽고 편하게 클래스와 상속을 구현할 수 있다. 개꿀~
class Tobacco {
constructor() {
console.log('initialize Tobacco');
}
inhale() {
console.log('Puff Puff');
}
exhale() {
console.log('Paaaaaah');
}
}
console.log(new Tobacco());
📌 주의할 점
class Tobacco {
constructor() {
console.log('initialize Tobacco');
}
inhale() {
console.log('Puff Puff');
}
exhale() {
console.log('Paaaaaah');
}
}
class Esse extends Tobacco {
cunstructor(type) {
super();
this.type = type;
}
}
console.log(new Esse('ChangeOne'));
extends
키워드 뒤에 상속받을 부모 클래스를 정의해준다.constructor()
생성자 메소드에서 super()
를 호출한다.📌 주의할 점
constructor()
생성자 메소드에서 반드시 this를 사용하기 전에 super()
를 먼저 호출해야한다.super()
호출이 먼저다.와 이제 뭔가 학교 다니면서 배웠던 객체 지향 프로그래밍 느낌이 난다...!
static
키워드를 사용해서 정적 메소드를 정의this
가 아닌 클래스 이름을 사용하여 접근 가능class Marlboro extends Tobacco {
constructor(type) {
super();
this.type = type;
}
static CreateVista() {
return new Marlboro('Vista');
}
}
클래스의 프로퍼티와 메소드들은 기본적으로 모두 public이다.
#
이라는 접두사를 추가하면 private 클래스 필드를 선언할 수 있다.class Marlboro extends Tobacco {\
#company;
constructor(type) {
super();
this.type = type;
this.#company = 'Philip Morris';
}
}
const marlboro_red = new Marlboro('Red');
console.log(marlboro_red.type); // 'Red';
console.log(marlboro_red.#company); // Uncaught SyntaxError: Private field '#company' must be declared ...