본문으로 바로가기

프로토타입, 생성자, 인스턴스

이 글에서는 인스턴스와 생성자의 prototype 속성, 프로토타입 객체의 constructor 속성에 대해 알아봅니다.



생성자, 프로토타입 객체, 인스턴스 관계도

함수 모델

일반적으로 모든 함수를 정의하면 다음과 같은 모델이 구성됩니다.

javascript
function fn() {
    var x = 10;
    var y = 20;
    return x + y;
}
fn();


위의 그림에서는 함수가 정의될 때 그 함수와 연결된 프로토타입이라는 객체가 함께 정의되는 것을 보여주고 있습니다.

생성자도 함수로서 이와 동일한 구조로 생성됩니다.


생성자 모델

이제 다음과 같이 정의된 생성자 Person을 봅니다.

javascript
function Person(name) {
    this.name = name;
}

Person을 파싱해서 정의하면 최종적으로 생성자 객체와 프로토타입 객체가 함께 정의됩니다.

그렇다고 두 존재가 같은 객체에 포함되는 것은 아닙니다.

생성자와 프로토타입 객체는 다음 그림처럼 별도로 존재하면서 서로에 대한 참조를 가지고 연결되어 있습니다.

그리고 공개 변수 스코프는 생성자의 멤버가 정의되는 곳으로서 현재 Person에는 생성자 멤버가 없는 관계로 생략해서 그림을 다음과 같이 간소해서 표현할 수 있을 것입니다.



Person 생성자와 그것의 프로토타입 객체가 별도의 객체라는 점을 강조하기 위해 생성자와 프로토타입 객체를 분리해서 표현되었습니다.

분리된 두 객체가 생성자의 prototype 속성과 프로토타입 객체의 constructor라는 속성으로 연결되어 있습니다.


생성자 객체와 프로토타입 객체가 생성되면 각각 prototype, constructor 공개 속성이 추가되고 서로에 대한 참조가 할당됩니다.


생성자와 프로토타입 객체는 별도의 객체로서 서로의 참조값을 prototype, constructor에 가지고 있다.


이제 new Person 으로 객체를 생성해 봅니다.

javascript
// Person 인스턴스 생성
var mySon = new Person('jaehee');

이 코드가 실행되고 나서 생성되는 인스턴스와 prototype, constructor의 관계를 그려보면 다음과 같습니다.


그림을 보면 mySon 인스턴스는 Person의 프로토타입 멤버를 상속한다고 표시되어 있습니다.


이렇게 생성자 객체, 프로토타입 객체 그리고 인스턴스는 구분돼야 합니다.

생성자, 프로토타입 객체는 함수를 정의하면 함께 정의되는 객체이고 인스턴스는 생성을 해서 얻게 되는 객체입니다.




프로토타입 객체

함수를 정의하면 함께 정의된다고 하는 프로토타입 객체에 대해 좀더 알아봅니다.

prototype 속성

위 예제 코드인 Person 생성자에는 prototype이라는 공개 속성이 있는데, 이 속성을 통해 프로토타입 객체에 접근할 수 있습니다.

javascript
Person.prototype

생성자의 프로토타입 객체에 접근해 멤버를 추가하거나 삭제하려면 반드시 생성자의 prototype 속성을 이용해야 합니다.

프로토타입 객체는 생성자별로 하나만 정의되므로 프로토타입 객체에 접근하는 데 생성자를 이용하고 있습니다.


하지만 인스턴스에서 직접 프로토타입 객체에 접근하는 방법은 없습니다.

따라서 다음과 같은 코드는 올바르지 않습니다.

javascript
mySon.prototype // 잘못된 코드

중요한 것은 직접 프로토타입 객체에 접근은 못해도 인스턴스를 통해서 생성자별로 정의되는 프로토타입 멤버에는 접근할 수 있습니다.


프로토타입 객체의 특징

프로토타입 객체도 자바스크립트 객체입니다.

즉, 프로토타입 객체도 (키, 값) 쌍으로 된 멤버를 추가할 수 있는 연관 배열 구조라는 것입니다.

따라서 프로토타입 객체에도 런타임에 멤버를 동적으로 추가할 수 있습니다.

참고로 프로토타입 객체에 포함된 멤버를 프로토타입 멤버라고 하며, 프로토타입 객체가 생성되면 constructor 라는 속성이 기본적으로 추가됩니다.


다음은 prototype 을 통해 Person의 프로토타입 메서드 멤버를 추가하는 코드입니다.

javascript
Person.prototype.getName = function () {
    return this.name;
}

이렇게 Person.prototype 을 통해 추가하면 그 멤버는 Person의 모든 인스턴스에서 사용할 수 있게 된다는 점입니다.

다시 말해, 모든 인스턴스는 해당 생성자가 가지고 있는 프로토타입 객체의 멤버를 상속받는다라고 할 수 있습니다.


이제 Person 인스턴스는 모두 getName에 접근할 수 있습니다.

Person 생성자의 코드를 정리하면 다음과 같습니다.

javascript
function Person(name) {
    this.name = name;
}
    
Person.prototype.getName = function () {
    return this.name;
}

var mySon = new Person('jaehee');
var yourSon = new Person('jeongin');

console.log(mySon.getName()); // jaehee 반환
console.log(yourSon.getName()); // jeongin 반환

위와 같이 프로토타입 멤버에서 this를 사용한다면 이 this는 현재 인스턴스를 나타닙니다.


프로토타입 멤버에서의 this 는 현재 인스턴스를 나타낸다.


위 코드에서 보다시피 getName에 접근하는 데Person.prototype.getName 처럼 생성자의 prototype을 통해 접근할 수 있을 뿐더러 mySon.getName 처럼 인스턴스를 통해서도 접근할 수 있습니다.

그러나 생성자의 prototype을 통해 접근하느냐 인스턴스를 통해 접근하느냐에 따라 다른 행동을 할 수 있습니다.

이와 관련된 설명은 뒤에서 알아보도로 하겠습니다.



프로토타입 멤버와 인스턴스 멤버 비교

각 인스턴스도 연관 배열 구조이므로 멤버를 동적으로 추가할 수 있습니다.

앞에서 생성된 인스턴스인 mySon, yourSon 에 멤버를 추가해 봅니다.

javascript
mySon.nickName = '재희 별명';
yourSon.nickName = '정진 별명';

위처럼 인스턴스에 새롭게 추가된 nickName 멤버는 인스턴스별로 관리됩니다.


자바스크립트에서는 객체의 멤버를 언제든지 조작할 수 있게 공개해 놓고 있습니다.

그래서 프로토타입 객체의 멤버도 언제든지 멤버를 추가, 제거, 대체할 수 있습니다.


프로토타입 객체는 함수를 메모리에 정의할 때 한 번만 생성됩니다.

프로토타입 멤버는 인스턴스별로 복사복이 존재하는 것이 아니라 해당 생성자에 하나만 존재하면서 그 생성자의 모든 인스턴스가 함께 공유하게 되는 것입니다.



프로토타입 멤버 편집의 비대칭

여기서 설명하는 프로토타입 멤버의 특징을 이해하지 못하면 자바스크립트 코드를 작성하다가 자칫 디버깅하는 데 많은 시간을 허비할 수 있습니다.

구조상으로는 분명한데 우리가 의도한 대로 되지 않을 수 있는 부분이 있기 때문입니다.


먼저 다음 코드를 예로 들어보겠습니다.

javascript
// 생성자 정의
function Person() {
}

// 인스턴스 생성
var mySon = new Person();
var yourSon = new Person();

// prototype을 통해 age 값 쓰기
Person.prototype.age = 6;

위에서 생성자 Person이 정의됐고 Person 인스턴스인 mySon, yourSon을 생성했습니다.

그리고 나서 Person의 프로토타입 객체에 age 라는 속성을 추가했습니다.


프로토타입 멤버 읽기

프로토타입 멤버를 다음과 같이 읽는 경우 그 결과는 다음과 같이 명백합니다.

javascript
// age 값 읽기
console.log(Person.prototype.age); // 6 출력
console.log(mySon.age); // 6 출력
console.log(yourSon.age); // 6 출력

mySon, yourSon에는 age가 없습니다.

따라서 mySon에서 age 속성 검색을 마치고 나면 프로토타입 객체로 이동해 age를 검색하고 그렇게 검색한 결과를 출력하기 때문에 모두 동일한 값인 6이 출력됩니다.


Person 프로토타입 객체에 정의된 멤버(이 경우 age)의 값을 "읽을"때는 다음의 두 표현을 모두 사용할 수 있습니다.

javascript
var age = Person.prototype.age;
var age = mySon.age;


프로토타입 멤버 추가하기

이제 다음과 같이 인스턴스를 통해 age에 값을 쓰는 코드를 보도록 합니다.

javascript
mySon.age = 7; // mySon의 age에 7를 쓴다.
yourSon.age = 8; // yourSon의 age에 8을 쓴다.

age는 인스턴스 멤버가 아니므로 mySon에서 age를 찾지 못합니다.

그러면 age를 mySon 인스턴스에 속성으로 추가할까, 아니면 프로토타입 객체의 멤버를 검색할까?


이를 알아보고자 앞에서 할당한 값을 다음과 같이 출력해 봅니다.

javascript
// 생성자 정의
function Person() {
}

// 인스턴스 생성
var mySon = new Person();
var yourSon = new Person();

// prototype을 통해 age 값 쓰기
Person.prototype.age = 6;


mySon.age = 7; // mySon의 age에 7를 쓴다.
yourSon.age = 8; // yourSon의 age에 8을 쓴다.

// age 값 읽기
console.log(Person.prototype.age); // 6 출력
console.log(mySon.age); // 7 출력
console.log(yourSon.age);// 8 출력

출력 결과를 보면 인스턴스를 통해 속성에 값을 쓸 때는 프로토타입 객체를 검색하지 않고 인스턴스에서만 속성을 검색한다는 것을 알 수 있습니다.

즉, mySon.age에 값을 할당하면 Person.prototype.age 의 값을 업데이트하는 것이 아니라 mySon에 age 속성을 추가해서 값을 할당합니다.

이제 코드처럼 인스턴스를 통해 age의 값을 출력하면 각 인스턴스의 age 값을 출력하게 됩니다.


이러한 메커니즘으로 설계된 이유는 하나의 생성자를 이용해 여러 인스턴스가 생성될 텐데, 인스턴스 하나에서 값을 수정하더라도 다른 객체에 영향을 미치는 것을 막기 위해서입니다.


인스턴스를 통해 멤버에 값을 쓰는 경우 멤버 검색 시 프로토타입 객체로까지 거슬러 올라가지 않는다.


그러나 어떤 상황에서는 모든 인스턴스의 값을 동시에 수정하고 싶은 경우가 있을 텐데, 이런 경우는 어떻게 할까?

그럴 때는 다음처럼 인스턴스를 통한 접근 대신 생성자의 prototype을 이용해 프로토타입 객체에 직접 접근해서 값을 변경하면 됩니다.

아래와 같이 prototype을 통해 속성의 값을 변경하면 이제 모든 인스턴스를 통해 age값을 읽어도 동일한 값이 출력됩니다.

javascript
function Person() {
}

// 인스턴스 생성
var mySon = new Person();
var yourSon = new Person();

// prototype을 통해 age 값 쓰기
Person.prototype.age = 10;

// age 값 읽기
console.log(Person.prototype.age); // 10 출력
console.log(mySon.age); // 10 출력
console.log(yourSon.age);// 10 출력


프로토타입 멤버에 값을 쓰고 싶다면 인스턴스가 아니라 생성자의 prototype 속성을 이용해야 한다.




생성자(constructor)

함수를 정의하면 프로토타입 객체가 정의되고 그 객체의 멤버로 constructor 속성을 가지고 있다고 설명했습니다.

constructor 속성은 프로토타입 객체를 생성했던 함수에 대한 참조를 나타냅니다.

이 프로토타입 객체의 constructor 속성은 상속을 통해 인스턴스에서 다음과 접근할 수 있습니다.

위의 Person 예제의 경우 mySon.construcctor 값은 Person을 가리키므로 다음 코드는 참이 될 것입니다.

javascript
// 생성자 정의
function Person() {
}

// 인스턴스 생성
var mySon = new Person();
var yourSon = new Person();

console.log(mySon.constructor == Person); // true 반환


앞에서 본 생성자, 프로토타입 객체, 그리고 인스턴스 관계도를 보면서 따져보면 다음과 같은 객체 비교는 모두 참이 됩니다.

javascript
console.log(mySon.constructor == Person.prototype.constructor); // true 반환
console.log(mySon.constructor == Person); // true 반환

여기서 주목할 점은 constructor 속성은 단순히 함수명을 나타내는 것이 아니라 함수 객체에 대한 참조라는 것입니다.

constructor 프로토타입 속성이 생성자 자체를 가리키는 참조이므로 이 값을 통해 함수를 호출할 수도 있고 따라서 다음과 같이 객체를 생성할 수 도 있습니다.

javascript
// 생성자 정의
function Person(name) {
    this.name = name;
}

var mySon = new Person('jaehee');

// constructor 속성을 통해 객체를 생성
var myGrandSon = new mySon.constructor('손자');
console.log(myGrandSon.name);

mySon.constructor는 생성자 객체를 가리키는 참조값이므로 () 연산자를 이용해 직접 호출할 수 도 있고 코드처럼 new와 함께 사용되어 객체를 생성할 수도 있습니다.


프로토타입 객체의 멤버인 constructor 속성은 해당 프로토타입 객체를 생성한 생성자 객체를 가리킨다.


prototype 속성처럼 constructor에 할당된 기본값도 런타임에 언제든지 변결될 수도 있습니다.

다음과 같이 mySon의 constructor에 Object 생성자를 할당할 수도 있습니다.

javascript
mySon.constructor = Object;

생성자, 프로토타입 객체, 그리고 인스턴스 관계도를 보면 인스턴스의 constructor를 통해서도 프로토타입 객체에 접근하는 방법이 있습니다.

만약 mySon 인스턴스를 통해 프로토타입 객체에 접근하려면 다음과 같이 할 수 있습니다.

javascript
mySon.constructor.prototype; // 인스턴스를 통해 프로토타입 객체에 접근


constructor는 앞에서 본 것처럼 상속을 통해 mySon에서 사용할 수 있게 됩니다.

따라서 다음과 같은 비교는 모두 프로토타입 객체를 나타내는 표현이 됩니다.

javascript
function Person(name) {
    this.name = name;
}

var mySon = new Person('jaehee');

console.log(mySon.constructor.prototype == Person.prototype); // true 반환



Jaehee's WebClub