이번 시간에는 자바스크립트식 객체 지향 프로그래밍(OOP, Object Oriented Programming)에 대해 알아보겠습니다.
생성자
지난 시간에 Date 객체를 new Date()
로 만들었던 것 기억하시나요? Date는 분명 객체라고 했는데 new를 붙이고 함수처럼 호출했습니다. 바로 자바스크립트 생성자(constructor) 함수입니다. 객체를 생성하는 함수를 생성자 함수라고 부릅니다. 다른 언어에서는 class가 있지만 자바스크립트에서는 없습니다. 생성자 함수가 그 역할을 대신합니다. 예를 들면 사람 생성자를 만들면 다음과 같습니다.
function Person(name, gender) {
this.name = name;
this.gender = gender;
this.sayHello = function() {
alert(this.name + ' said "hello"');
}
this.... // 사람의 속성과 메소드를 더 정의할 수 있습니다.
}
함수를 만들 때처럼 function을 쓰긴 했는데 함수와는 달리 대문자로 시작하게 만듭니다. 이게 규칙입니다. 생성자를 바탕으로 실제 사람 객체를 만들 수 있습니다. new라는 키워드를 사용해서 호출합니다. new 생성자(인자);
이렇게 하면 됩니다. Zero와 그의 친구 Hero를 만들어보죠!
var zero = new Person('Zero', 'm'); // Person {name: 'Zero', gender: 'm'}
var hero = new Person('Hero', 'f'); // Person {name: 'Hero', gender: 'f'}
zero.sayHello(); // 'Zero said "hello"'
hero.sayHello(); // 'Hero said "hello"'
하나의 Person 생성자를 바탕으로 zero와 hero 두 사람 객체를 만들었습니다. 그리고 이 객체들은 공통적으로 sayHello라는 메소드를 갖고 있습니다.
function Person(name, gender) {
this.name = name;
this.gender = gender;
this.sayHello = function() {
alert(this.name + ' said "hello"');
}
}
다시 보면요. Person 옆에 (name, gender)는 처음 만들 때 매개변수를 받는 부분임을 알 수 있습니다. 그렇게 받은 매개변수들을 this.name
, this.gender
에 저장합니다. this는 바로 생성자 함수 자신을 가리킵니다. 이렇게 this에 저장된 것들은 new를 통해 객체를 만들 때 그 객체에 적용됩니다.
프로토타입
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
Person.prototype.sayHello = function() {
alert(this.name + ' said "hello"');
};
이번에는 다른 건 위의 코드와 같은데 this.sayHello
대신에 Person.prototype
에 sayHello를 넣었습니다. prototype은 처음보죠? prototype 객체는 사전 그대로 원형을 뜻합니다. 원래의 모습이죠. 같은 생성자로부터 만들어진 객체들은 모두 이 원형 객체를 공유합니다. 따라서 Person의 prototype 객체에 sayHello라는 메소드를 넣으면 Person 생성자로 만든 모든 객체는 이 메소드 사용이 가능합니다. 공유하고 있기 때문이죠.
그런데 this.sayHello
보다 prototype에 Person.prototype.sayHello
로 넣는 게 더 효율적입니다. prototype은 모든 객체가 공유하고 있어서 한 번만 만들어지지만, this에 넣은 것은 객체 하나를 만들 때마다 메소드도 하나씩 만들어지기 때문에 불필요한 메모리 낭비가 발생합니다.
아예 메소드뿐만 아니라 속성까지 다 prototype에 넣기도 합니다. 어떤 때 그러는지는 링크 를 참조하세요.
prototype과 __proto__
new Person('Nero', 'm'); // Person {name: "Nero", gender: "m", __proto__: Object}
Nero라는 새 사람 객체를 만들었더니 그 안에 처음보는 __proto__라는 객체가 있습니다. 눌러보니
{
constructor: function Person(name, gender),
sayHello: function() {},
__proto__: Object
}
constructor과 우리가 추가한 sayHello 그리고 또다시 __proto__가 있네요.
__proto__가 바로 실제 객체를 만들 때 생성자의 prototype이 참조된 모습입니다. 생성자의 prototype을 참조하기 때문에 __proto__와 prototype은 같습니다. 아까 Person.prototype.sayHello
를 했던 것이 들어있죠. 추가로 constructor(생성자)에 대한 정보까지 들어있습니다. __proto__ 안에 들어있는 __proto__는 지금은 생각하지 않으셔도 됩니다. 이 강좌 에 나와있습니다.
정리하면
- constructor는 생성자 함수 그 자체를 가리킴
- prototype은 생성자 함수에 정의한 모든 객체가 공유할 원형
- __proto__는 생성자 함수를 new로 호출할 때, 정의해두었던 prototype을 참조한 객체
- prototype은 생성자 함수에 사용자가 직접 넣는 거고, __proto__는 new를 호출할 때 prototype을 참조하여 자동으로 만들어짐
- 생성자에는 prototype, 생성자로부터 만들어진 객체에는 __proto__
- 따라서 사용자는 prototype만 신경쓰면 된다. __proto__는 prototype이 제대로 구현되었는지 확인용으로 사용한다.
prototype, __proto__와 constructor의 관계
prototype과 constructor는 부모자식 관계라고 생각하면 됩니다. Person.prototype.constructor === Person;
입니다. 또한 Person.prototype === (Person생성자로 만들어진 객체).__proto__;
이기 때문에 (Person생성자로 만들어진 객체).__proto__.constructor === Person;
도 성립합니다.
상속의 필요성
위와 같이 생성자 함수를 만들고, new 키워드를 통해 객체도 만들었습니다. Person 생성자 외에도 Vehicle이나 Machine 같은 생성자를 만들 수도 있겠죠?
function Vehicle(name, speed) {
this.name = name;
this.speed = speed;
}
Vehicle.prototype.drive = function () {
console.log(this.name + ' runs at ' + this.speed)
};
이렇게 Vehicle 생성자를 만들었습니다. drive 메소드로 달릴 수 있네요! 근데 drive 메소드 하나밖에 없으니 좀 심심하죠? boost라는 메소드를 만들어서 최고 속도로 달릴 수 있는 기능을 추가하면 괜찮을 거 같은데요. 그래서 Vehicle.prototype.boost = function () {...}
이렇게 추가하자니 한 가지가 걸립니다. Vehicle 안에는 트럭, SUV, 스포츠카, 세단 등 많은데 트럭이 boost하는 건 좀 이상하지 않나요?
이럴 때 상속이 필요합니다. 상속은 외국에서 확장(extend)이라고도 표현합니다. 즉 부모 생성자의 기능을 물려받는 동시에 새로운 기능을 추가할 수도 있는거죠.
다음 시간에는 객체를 상속하는 방법에 대해 알아보겠습니다. Vehicle 생성자의 기능을 상속하고 확장한 Sedan 객체를 만들어봅시다!