본문 바로가기

Programming/Javascript

10. Javascript 프로토타입 체이닝 (6) - 디폴트 프로토타입 객체의 변경

 

 

함수의 프로토타입 객체를 덮어쓴다고?

앞선 아티클에서 살펴본 내용이지만 다시 언급하자면, 기본 프로토타입 객체는 함수가 생성될 때 같이 생성이 되고, 함수의 prototype 프로퍼티에 연결됩니다(함수.prototype). 그리고 여기에는 constructor 프로퍼티가 존재하고 이는 함수 자신과 연결된다고 했었습니다. 더불어 프로토타입 함수가 포함되지요. 

 

그런데 기본적으로 이러한 구성을 가진 함수의 프로토타입 객체는, 일반 객체로 변경할 수 있다는 특징을 가지고 있습니다. 즉 우리가 위에서 설명한 프로토타입 객체의 구성을 사용자가 정의한 객체로 덮어쓸 수 있다는 것이죠. 그런데, 이런 성질을 어디다 쓸까요? 괜히 애써 배운 객체 구조만 꼬이게 될 것 같은데 말이죠. 

 

이런 프로토타입 객체의 성질은 추후 배워볼 상속을 구현하는 데 사용하게 됩니다. JAVA나 C#과 달리 별도의 extends 등의 키워드가 없기 때문에 이 성질을 이용하게 될 텐데, 객체지향 프로그래밍 파트에서 자세히 배워보겠습니다. 우선, 중요한 것은 이번 아티클에서 함수의 프로토타입 객체를 일반 객체로 덮어써 변경하게 되면 어떤 일이 구조적으로 벌어지는지 확인하겠습니다. 

 

여기서 프로토타입 객체의 동적 변경 과정에서 알아둬야 할 특징이 있습니다. 어떤 (생성자)함수의 프로토타입 객체를 동적으로 선언해 변경해 줄 경우, 변경 전에 new로 생성한 객체는 [[Prototype]] 링크를 그대로 유지하는데 반해 변경 후에 new로 생성한 객체는 변경된 객체로 [[Prototype]]링크를 연결하게 된다는 점입니다. 

 

예제를 통해서 이러한 특성을 하나씩 짚어가 보도록 합시다. 생각보다 복잡한 내용이 될 수 있으니 차근차근 살펴보도록 하겠습니다.

 

 

// 생성자 함수
function Rapper(rapName) {
    this.rapName = rapName;
}

// 디폴트 prototype의 상태에서 constructor 출력
console.log(Rapper.prototype.constructor);
// [Function: Rapper]

// 디폴트 prototype의 상태에서 객체 생성
var dean = new Rapper('DEAN');

// 디폴트 prototype 변경
Rapper.prototype = {
    country: 'KOREA'
};

// 변경된 prototype의 상태에서 constructor 출력
console.log(Rapper.prototype.constructor);
// [Function: Object]

 

우선 위의 예제를 살펴봅시다. 종전처럼 생성자 함수 Rapper를 선언했고, 프로토타입 객체의 constructor를 출력해 보았습니다. 당연히 결과는 함수 자기 자신인 Rapper를 참조하고 있기 때문에 함수 Rapper가 출력됩니다. 그리고 그 상태에서 우선 새로운 객체 dean을 선언했습니다. 

 

그리고 이제부터가 본론입니다. 디폴트 프로토타입 객체를 동적으로 선언하는 코드를 볼 수 있습니다. Rapper.prototype에 새로운 임의 객체를 대입하였고, 해당 객체는 country라는 한 개의 프로퍼티를 갖고 있는 것으로 선언했습니다. 그리고 그 다음, 다시 동일하게 생성자 함수의 프로토타입 객체의 constructor를 출력해 보았습니다. 결과는 어떻게 되었나요? 생성자 함수가 Object인 것으로 출력되었습니다.

 

왜 Object가 출력되었는지 알아보겠습니다. 우선, Rapper의 프로토타입 객체에는 알다시피 constructor와 프로토타입 메서드가 자동으로 할당되어 있는 상태입니다(물론, 사용자가 별도로 선언하지 않았으므로 프로토타입 메서드는 없는 상태). 그런데 여기에 새로운 임의 객체를 할당함으로 인해서 이러한 디폴트 프로토타입 객체는 모두 사라지고, 프로퍼티 country를 가진 새로운 객체만 참조하게 되는 상태가 되었습니다. 

 

이 상태에서 Rapper의 프로토타입 객체에 있는 프로퍼티 constructor를 출력하라고 지시하면 어떤 일이 발생할까요? 바로 프로토타입 체이닝 원칙을 따르게 됩니다. Rapper.prototype 객체 안의 consturctor 객체를 출력하려고 하는데 위에서 설명했듯이 새로 선언한 객체에는 constructor 프로퍼티가 없는 상태입니다. 그럼 프로토타입 체이닝 원칙에 따라야 링크를 따라가야 하는데...새로 선언한 객체는 객체 리터럴 방식으로 선언되었죠? 그럼 해당 객체의 [[Prototype]]링크는 어디로 연결되어 있을까요? 리터럴로 선언한 객체이므로 역시 동일하게 Object.prototype으로 연결되어 있습니다. 그럼 링크를 타고 Object.prototype 내에서 consturctor 프로퍼티를 찾게 되고, 바로 이 Object.prototype.constructor를 출력하게 되는 것입니다. 그리고 이 프로퍼티는 생성자 함수 자신인 Object를 참조하고 있으므로 Object가 출력되는 것이죠.

 

 

자, 이제 위의 예제 코드에 이어서 다음 라인들을 추가 작성해 보겠습니다. 

 

var gaeko = new Rapper('GAKEKO');

console.log(dean.country);
console.log(gaeko.country);

console.log(dean.constructor);
console.log(gaeko.constructor);

// 출력
// undefined
// KOREA
// [Function: Rapper]
// [Function: Object]

 

자, 위에서 얘기한 Rapper의 프로토타입 객체를 수정한 이후에 gaeko라는 객체를 새롭게 생성했습니다. 그리고 dean과 gaeko 객체에서 각각 동일한 프로퍼티를 출력을 시도했습니다. 결과를 살펴볼까요? 

 

각 객체에서 Rapper의 prototype에 선언되었을 것이라고 가정한 프로퍼티, country와 constructor를 각각 출력을 시도했습니다. 우선 country를 볼까요? dean 객체는 undefined, gaeko 객체는 KOREA가 출력되었습니다. 그리고 constructor 객체는 dean의 경우 생성자 함수 Rapper, gaeko의 경우 생성자 함수 Object가 출력되었습니다. 

 

앞서 우리는 함수의 프로토타입 객체를 동적으로 변경하게 되면, 변경 이전에 생성한 객체는 그대로 [[Prototype]] 링크를 유지하고 변경 이후에 생성한 객체는 [[Prototype]] 링크가 수정된 프로토타입 객체로 변경된다고 설명했습니다. 이 내용이 그대로 적용된 것이죠. dean은 디폴트 Rapper 프로토타입 객체를 [[Prototype]] 링크로 연결한 상태이기 때문에 country는 존재하지 않으며 constructor를 출력하라 명령하면 기존의 디폴트 프로토타입 객체의 Rapper.prototype.constructor를 출력하게 됩니다. 

 

하지만 gaeko의 경우 새로 선언된 Rapper의 프로토타입 객체를 링크로 연결한 상태이므로 country가 출력되고, constructor는 없기 때문에 프로토타입 체이닝을 따라 Object의 프로토타입 객체에 존재하는 constructor를 출력하는 것입니다. 

 

* dean.constructor / gaeko.constructor / dean.country / gaeko.country 라는 명령어가 혹시 생소하다면, 프로토타입에 정의된 프로퍼티들은 자식 객체가 그대로 쓸 수 있다는 사실을 잠시 잊으신 겁니다! 

 

* 위 예제의 구조를 그림으로 구조를 표현하면 아래와 같습니다. 

prototype object channing