본문으로 바로가기

JS : prototype constructor inheritance & constructor stealing

이 포스팅에서는 예제를 통하여 생성자를 상속하고 생성자를 훔치는 코드에 대해 살펴보도록 합니다.




prototype constructor inheritance

다음의 예제 코드를 살펴보도록 합니다.

javascript
function Rectangle(length, width) {
	this.length = length;
	this.width  = width;
}

Rectangle.prototype = {
	getArea : function () {
		return this.length * this.width;
	},
	toString: function () {
		return "[Rectangle : " + this.length + "x" + this.width + "]";
	}
};

// inherits from Rectangle
function Square(size) {
	this.length = size;
	this.width  = size;
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

Square.prototype.toString = function () {
	return "[Square : " + this.length + "x" + this.width + "]";
};

var rect = new Rectangle(5, 10);
var square = new Square(6);

console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36

console.log(rect.toString()); // [Rectangle : 5x10]
console.log(square.toString()); // [Square : 6x6]

console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true

console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true


이 코드에는 RectangleSquare 라는 두 개의 생성자가 있습니다.

Square 생성자의 prototype 프로퍼티는 Rectangle 의 인스턴스가 설정(Square.prototype = new Rectangle();)되어 있습니다.

여기까지 설정할 때는 인수가 필요없으므로 Rectangle 에는 아무런 인수도 전달하지 않았습니다.

만약 이때 인수를 사용했다면 모든 Square 인스턴스가 같은 크기로 설정되었을 것입니다.

이 방식으로 프로토타입 체인을 변경할 대는 아무런 인수가 전달되지 않아도 에러가 발생하지 않도록 생성자를 주의해서 작성해야 하며(인수가 반드시 전달되어야 하는 생성자를 많이 작성한다), 생성자는 어떤 종류의 전역 상태이든 수정하지 않아야 합니다.

전역 변수 등을 사용해 생성된 인스턴스 개수를 추적하는 것이 한 예가 될 수 있습니다.

constructor 프로퍼티는 Square.prototype 을 덮어쓴 후에 원래 값으로 돌려놓아야 합니다.

이 과정이 끝난 후에는 Rectangle 의 인스턴스인 rect 와 Square 의 인스턴스인 square 를 생성합니다.

두 객체에는 Rectangle.prototype 에서 상속받은getArea() 라는 메소드가 존재합니다.

square 변수는 Square 의 인스턴스이자 RectangleObject 의 인스턴스이기도 합니다.

instanceof 연산자는 프로토타입 체인을 사용해 객체의 타입을 확인하기 때문에 이 변수는 세 가지 타입으로 인스턴스로 나타납니다.

사실 Square.prototype 은 굳이 Rectangle 객체로 다시 정의하지 않아도 되지만 Square 에 불필요한 동작을 Rectangle 생성자에서 하는 것도 아닙니다.

Square.prototypeRectangle.prototype 은 단지 상속이 어떻게 일어나는지 보여주기 위해 연결했을 뿐입니다.

따라서 Object.create() 를 사용하면 위 예제를 더 단순하게 만들 수도 있습니다.

javascript
// Rectangle 을 상속한다.
function Square(size) {
	this.length = size;
	this.width  = size;
}

Square.prototype = Object.create(Rectangle.prototype, {
	constructor : {
		configurable : true,
		enumerable : true,
		value : Square,
		writable : true
	}
});

Square.prototype.toString = function () {
	return "[Square : " + this.length + "x" + this.width + "]";
};

이 코드에서 Square.prototype 에는 Rectangle.prototype 을 상속한 새로운 객체가 할당되었으며 Rectangle 생성자는 한 번도 호출되지 않았습니다.

다시 말해, 인수 없이 이 생성자를 호출하면 에러가 발생할까 싶어 불안해하지 않아도 된다는 뜻이 됩니다.

그럼에도 이 코드는 앞서 보았던 코드와 완전히 똑같이 동작합니다.

프로토타입 체인은 그대로 남아있으므로 Square 의 인스턴스는 모두 Rectangle.prototype 을 상속받으며 인스턴스의 constructor 프로퍼티도 잘 복원되어 있습니다.


프로토타입에 프로퍼티를 추가하는 것보다, 먼저 프로토타입 재정의가 이루어져야 합니다.

그렇지 않으면 프로토타입을 다시 정의할 대 추가했던 메소드가 사라지게 됩니다.



constructor stealing

자바스크립트에서 상속은 프로토타입을 통해 이루어지기 때문에 객체 의 상위타입 생성자를 호출하지 않아도 됩니다.

상위 타입의 생성자를 하위 타입의 생성자에서 호출하려면 자바스크립트의 동작 방식에 대해 이해하고 이를 활용해야 합니다.

함수를 호출할 때 다른 this 값을 사용하도록 만드는 call()apply() 메소드를 이용할 수 있습니다.

생성자 훔치기(constructor stealing) 가 하는 일도 이와 똑같습니다.

하위타입 생성자에서 call() 이나 apply() 를 사용하며 새로 생성된 객체를 인수로 전달하면 상위타입 생성자를 호출할 수 있습니다.


다음의 예제는 직접 작성한 객체에서 상위타입 생성자를 훔치는 코드입니다.

javascript
function Rectangle(length, width) {
	this.length = length;
	this.width  = width;
}

Rectangle.prototype.getArea = function () {
	return this.length * this.width;
};

Rectangle.prototype.toString = function () {
	return "[Rectangle " + this.length + "*" + this.width + "]";
};

// Rectangle 에서 상속한다.
function Square(size) {
	Rectangle.call(this, size, size);
	
	// 여기서 새 프로퍼티를 정의하거나 기존 프로퍼티를 다시 작성할 수 있다.
}

Square.prototype = Object.create(Rectangle.prototype, {
	constructor : {
		configurable : true,
		enumerable : true,
		value : Square,
		writable : true
	}
});

Square.prototype.toString = function () {
	return "[Square : " + this.length + "x" + this.width + "]";
};

var square = new Square(7);

console.log(square.length); // 7
console.log(square.width); // 7
console.log(square.getArea()); // 49

Square 생성자는 Rectangle 생성자를 호출하면서 this 와 동시에 size 를 두 차례 인수로 사용(하나는 length 에 다른 하나는 width 에 저장된다) 했습니다.

따라서 lengthwidth 프로퍼티의 값이 size 와 같은 객체가 새로 생성됩니다.

이 방법을 사용하면 상속받을 생성자에서 정의한 프로퍼티를 다시 정의하는 것을 방지할 수 있습니다.

상위타입 생성자를 호출한 뒤에 새 프로퍼티를 추가하거나 기존 프로퍼티를 수정하면 됩니다.


생성자를 이렇게 두 단계로 나누어 실행하면 직접 만든 두 객체 간에 상속이 이루어질 때 유용하게 사용할 수 있습니다.

생성자의 프로토타입을 수정할 일은 늘 있겠지만 하위타입 생성자 안에서 상위타입 생성자도 함께 호출해야 할 수도 있습니다.

일반적으로 프로토타입 상속은 메소드 상속을 위해 사용하고 생성자 훔치기는 프로퍼티 상속을 위해 사용 하는 경우가 많습니다.

이 방식은 클래스 기반 언어의 클래스 상속을 흉내 냈기 때문에 이를 가리켜 의사 클래스 상속(pseudoclassical inheritance)이라고 부릅니다.




Jaehee's WebClub