자바스크립트: 프로토타입
객체(Object)
자바스크립트는 객체 기반 스크립트 언어입니다. 객체(Object)
는 데이터와 데이터의 동작을 모두 포함하고 있는 개념으로, 자신의 정보를 가지고 있는 독립적인 주체입니다. 자바스크립트에서 원시타입을 제외한 모든 값은 객체로 구성되어 있습니다.
자바스크립트의 객체는 키(key)
와 값(value)
로 구성된 프로퍼티(Property)
의 집합입니다. 프로퍼티의 값으로는 일급객체인 함수를 포함하여 모든 값을 사용할 수 있습니다. 프로퍼티의 값이 함수인 경우, 일반 함수와 구분하기위해 메소드(method)
라고 부릅니다.
프로토타입
자바(JAVA)와 같은 클래스 기반 객체 지향 언어의 경우 클래스를 정의하고 new 연산자를 통해 인스턴스를 생성하는 방식으로 객체를 생성합니다. 하지만 자바스크립트는 클래스라는 개념이 없으며 객체의 원형인 프로토타입을 복사하여 새로운 객체를 생성합니다.
자바스크립트에서 함수를 정의하고 파싱단계에 들어가면 함수의 멤버로 prototype
속성을 가지며, 이 속성은 함수 이름으로 생성된 프로토타입 객체를 참조합니다.
함수 정의
function User() {};
프로토타입 객체는 constructor
속성을 가지며, 해당 속성은 함수를 참조합니다.
객체의 내부 구조
프로토타입 객체는 new 연산자를 통해 생성되는 모든 User 객체의 원형이 되는 객체입니다.
객체 생성
function User() {};
const user1 = new User();
const user2 = new User();
위의 코드와 같이 new를 통해 User 객체를 생성한다면 해당 객체는 __proto__
속성을 통해 User 함수의 프로토타입 객체를 참조하고 링크됩니다.
프로토타입 객체 참조
이러한 구조를 통해 프로토타입 객체에 메소드를 동적으로 추가해 줄 수 있습니다.
메소드 추가
function User(name) {
this.name = name;
};
User.prototype.printName = function() {
console.log(this.name);
}
const user1 = new User('루비스코');
const user2 = new User('스팟');
만약 User 프로토타입 객체에 동적으로 메소드를 추가한다면 user1 객체와 user2 객체에 해당 메소드가 없는 경우 __proto__ 프로퍼티를 통해 상위 객체에 해당 메소드가 있는지 탐색합니다.
user1, user2 객체에는 printName
이라는 메소드가 존재하지 않으므로 __proto__ 프로퍼티가 참조하는 User 프로토타입 객체에서 printName 메소드를 찾습니다.
프로토타입 체인
코드의 재사용
클래스 기반 객체지향 언어인 자바의 경우 상속을 통해 코드의 재사용이 가능합니다. 자바스크립트는 클래스가 없는 프로토타입 기반 언어이지만 프로토타입 체인을 통해 코드의 재사용이 가능합니다.
프로토타입을 통해 상속을 하는 방법에는 크게 classical
과 prototypal
2가지가 있습니다.
classical 방식
classical 방식은 new 연산자를 통해 생성된 객체를 사용하여 상속을 하는 방식입니다. 자바에서 new 연산자를 통해 인스턴스를 생성하는 방식과 유사합니다.
위에서 작성한 코드처럼 부모에 해당하는 함수를 사용하여 객체를 생성합니다. 이렇게 생성된 부모 객체를 자식에 해당하는 함수의 프로토타입 프로퍼티가 참조하도록 한다면 프로토타입 체인을 형성하여 코드의 재사용이 가능해집니다.
classical 방식
function Person(name) {
this.name = name || "anonymous";
};
Person.prototype.getName = function() {
return this.name;
}
function Korean(name) {}
Korean.prototype = new Person();
const rubisco = new Korean("루비스코");
console.log(rubisco.getName());
다이어그램
부모함수는 Person
이며, 프로토타입 프로퍼티를 통해 getName
메소드를 추가했습니다.
자식함수는 Korean
이고, 해당 함수 안에 프로토타입 프로퍼티의 참조를 Person 함수로 생성된 객체가 되도록 변경했습니다.
이제 new 연산자를 통해 Korean 함수의 객체인 rubisco
를 생성합니다. 그러면 rubisco 객체가 Person 객체를 참조하고, Person 객체는 Person 프로토타입 객체를 참조하면서 위에 다이어그램과 같은 프로토타입 체인을 형성합니다. rubisco 객체는 getName 메소드를 가지고 있지 않지만 __proto__를 통해 부모인 Person 프로토타입 객체의 getName 메소드를 사용할 수 있습니다. 즉, getName 메소드를 상속받습니다.
하지만 이 코드에는 문제점이 있습니다. Korean 함수를 통해 rubisco 객체를 생성할 때 name 파라미터로 루비스코
를 넘겨주었지만 부모 객체를 생성할 때 파라미터를 전달하지 않아서 name 속성은 어떤 파라미터를 주더라도 anonymous
로 고정됩니다.
출력 결과
아래와 같이 코드를 수정해보겠습니다.
classical 방식
function Person(name) {
this.name = name || "anonymous";
};
Person.prototype.getName = function() {
return this.name;
}
function Korean(name) {
Person.apply(this, arguments);
}
const rubisco = new Korean("루비스코");
console.log(rubisco.name);
Korean 함수 내부에 apply
메소드를 사용하여 부모객체 Person의 this를 Korean 함수의 this에 바인딩합니다. 이것은 부모의 속성을 자식 함수 안에 모두 복사하여 자신의 속성으로 만들어버립니다.
다이어그램
Korean 함수를 통해 rubisco 객체를 생성하면 name 파라미터를 통해 Person 객체를 생성하고, Person 객체의 this를 Korean 함수의 this에 바인딩하여 Korean 객체를 생성합니다. 즉, Korean 객체는 Person 객체의 생성자 함수를 빌려 생성된 객체입니다.
위에 다이어그램에서 보는 것과 같이, 참조를 통해 Person 프로토타입 객체의 name 프로퍼티에 접근하는 것이 아니라 Korean 객체의 name 프로퍼티에 직접 접근합니다.
출력 결과
이 경우 Person 객체의 멤버변수는 상속받을 수 있지만, Person 함수의 프로토타입 객체에 대한 링크가 없기때문에 getName 메소드는 상속받지 못합니다.
아래 코드로 수정하여 Korean 객체를 Person 프로토타입 객체에 링크해줍시다.
classical 방식
function Person(name) {
this.name = name || "anonymous";
};
Person.prototype.getName = function() {
return this.name;
}
function Korean(name) {
Person.apply(this, arguments);
}
Korean.prototype = new Person();
const rubisco = new Korean("루비스코");
console.log(rubisco.getName());
다이어그램
rubisco 객체의 __proto__ 속성이 Person 프로토타입 객체를 참조하므로 getName 메소드를 사용할 수 있습니다.
출력 결과
하지만 이 코드에도 문제점이 있습니다. 부모의 생성자 함수를 두 번 호출합니다. 다이어그램을 확인하면 name 프로퍼티가 Person 객체에도, Korean 객체인 rubisco 객체에도 존재합니다.
이 문제를 해결하려면 Korean 객체의 prototype 프로퍼티가 new 연산자를 통해 생성된 Person 객체를 참조하는 것이 아니라 Person 프로토타입 객체를 참조하도록 하면 됩니다.
classical 방식
function Person(name) {
this.name = name || "anonymous";
};
Person.prototype.getName = function() {
return this.name;
}
function Korean(name) {
this.name = name;
}
Korean.prototype = Person.prototype;
const rubisco = new Korean("루비스코");
console.log(rubisco.getName());
다이어그램
new 연산자를 통해 rubisco 객체를 생성하면 name 프로퍼티가 설정되고, Korean 프로토타입 객체는 Person 프로토타입 객체를 참조하므로 getName 메소드 역시 상속받게 됩니다.
출력 결과
prototypal 방식
prototypal 방식은 리터럴 또는 Object 객체의 create
메소드를 사용하여 객체의 생성과 동시에 프로토타입 객체를 설정합니다.
classical 방식보다 간결하게 상속을 구현할 수 있기때문에 해당 방식을 많이 사용합니다.
prototypal 방식
const person = {
name: 'anonymous',
getName: function() {
return this.name;
}
}
const rubisco = Object.create(person);
rubisco.name = '루비스코';
console.log(rubisco.getName());
출력 결과
Object.create는 1번째 파라미터는 부모객체로 사용할 객체를, 2번째 파라미터는 선택적 파라미터로 자식객체의 프로퍼티로 추가됩니다.
아래는 name 프로퍼티를 getter와 setter로 커스텀하여 재정의 했습니다.
prototypal 방식
const person = {
name: 'anonymous',
getName: function() {
return this.name;
}
}
const rubisco = Object.create(person, {
name: {
get: function() {
return `이름: ${this._name}`;
},
set: function(name) {
this._name = name;
}
}
});
rubisco.name = '루비스코';
console.log(rubisco.getName());
출력 결과
클래스(Class)
자바스크립트에는 클래스가 존재하지 않았지만, ES6부터 클래스 문법이 추가되었습니다. 새로운 문법은 아니며 프로토타입을 사용하는 Syntatic Sugar 입니다.
extends
키워드를 통해 상속을 받을 수 있으며, 생성자 메소드는 constructor
입니다.
class 사용
class Person {
constructor() {
this.name = 'anonymous';
}
getName() {
return this.name;
}
}
class Korean extends Person {
constructor(name) {
super();
this.name = name;
}
}
const rubisco = new Korean('루비스코');
console.log(rubisco.getName());
출력 결과
클래스 문법은 자바에서 클래스를 작성하는 것과 많이 유사합니다. 부모 객체의 this는 super를 통해 접근할 수 있습니다.
타입을 붙이는 것을 제외하면 타입스크립트에서 클래스를 작성하는 것과 동일하므로 클래스 문법에 대한 자세한 내용은 타입스크립트 문법을 참고해주세요.