상속

이효범·2022년 5월 4일
0
post-thumbnail

객체지향 프로그래밍을 배울 때 객체를 생성하는 방법을 먼저 배우고 그 다음으로 상속을 배운다. 전통적인 객체지향 언어의 클래스는 다른 클래스로부터 프로퍼티를 상속할 수 있다. 하지만 클래스 같은 구조와 관계를 정의할 수 없는 자바스크립트에도 객체 간에 상속이 가능하다. 상속을 위한 메커니즘은 우리에게 이미 익숙해진 기술인 프로토타입을 사용한다.

프로토타입 체이닝과 Obejct.prototype

자바스크립트에서 제공하는 상속 방법은 프로토타입 체이닝(prototype chaining) 또는 프로토타입 상속(prototypal inheritance)이라고 부른다. 앞어 배웠듯이 프로토타입에 정의된 프로퍼티는 자동으로 모든 객체에서 사용할 수 있게 되는데 이 형태가 상속과 유사하다. 객체 인스턴스는 프로토타입으로부터 프로퍼티를 상속받는다. 프로토타입 또한 하나의 객체이므로 프로토타입에도 자신만의 프로토타입이 있으며 프로토타입으로부터 프로퍼티를 상속받는다. 이러한 특성을 가리켜 프로토타입 체인(prototype chain)이라 부른다. 객체는 객체의 프로토타입을 상속받고 프로토타입 역시 자신의 프로토타입을 상속받는다. 프로토타입 체인의 끝에 다를 때까지 이런 관계가 계속 이어진다.

우리가 직접 작성한 객체를 비롯해 모든 객체는 별도로 정해주지 않으면 Object를 상속받는데(자세한 내용은 후반부에 다루겠다), 이는 엄밀히 말하면 Object.prototype을 상속받는 것이다. 객체 리터럴을 사용해 정의된 객체의 [[Prototype]] 프로퍼티는 모두 Object.prototype을 가리킨다. 이를 바꿔 생각하면 아래 예제의 book 객체처럼 Object.prototype으로부터 프로퍼티를 상속받는다는 뜻이 된다.

let book = {
 title: "객체지향 자바스크립트" 
};

let prototype = Object.getPrototypeOf(book);

console.log(prototype === Object.prototype);  // true

여기서 book의 프로토타입은 Object.prototype과 같다. 다른 코드를 더 작성하지 않아도 book 객체의 프로토타입은 Object.prototype을 자동으로 가리키도록 만들어져 있다. 따라서 book은 자동으로 Object.prototype의 메소드도 가져온다.

Obejct.prototype에서 메소드 상속

이전 글에서 사용했던 여러 메소드는 사실 Obejct.prototype에 정의되어 있어서 모든 객체로 상속된 것이다. 상속된 메소드는 다음과 같다.

  • hasOwnProperty() : 주어진 이름의 고유 프로퍼티가 존재하는지 확인한다.
  • propertyIsEnumerable() : 고유 프로퍼티가 열거 가능한지 확인한다.
  • isPrototypeOf() : 객체가 다른 객체의 프로토타입인지 확인한다.
  • valueOf() : 객체를 표현하는 값을 반환한다.
  • toString() : 객체를 표현하는 문자열을 반환한다.

이 다섯 개 메소드는 모든 객체에 상속되므로 어느 객체에서든 사용할 수 있다. 이 중 valueOf()와 toString()은 자바스크립트에서 객체를 일관성 있는 방식으로 다루기 위해 중요하며, 때로는 직접 작성해야 할 수도 있다.

valueOf()

valueOf() 메소드는 객체에 연산자를 사용할 때 호출된다. valueOf()는 객체 그 자체를 반환하는 것이 일반적이다. 원시 래퍼 타입의 valueOf() 메소드에서 원시 값을 반환하도록 재정의 되어 있다. 다시 말해 String 타입은 문자열을 반환하고, Boolean 타입은 불리언 값을 반환하고 Number 타입은 숫자를 반환한다. 이와 비슷하게 Date 객체의 valueOf() 메소드도 밀리초 단위의 에포크 시간(epoch time)을 반환하도록 수정되어 있으며(결과는 Date.prototype.getTime()을 실행한 것과 같다), 덕분에 날짜를 다음과 같이 비교할 수 있다.

let now = new Date();
let earlier = new Date(2022, 1, 1);

console.log(now > earlier);  // true

이 예제에서 now는 현재 시각을 가리키는 Date 객체이며 earlier는 과거의 시점을 가리키는 Date 객체이다. 날짜 객체에 좌변이 큰지 확인하는 부등호 연산자 > 를 사용하면 비교를 수행하기 전에 각 객체의 valueOf() 메소드가 호출된다. valueOf() 덕분에 한 날짜에서 다른 날짜를 빼서 두 날짜 간의 차이를 구할 수도 있다.

직접 작성한 객체에 연산자를 사용하고 싶다면 valueOf() 메소드도 정의해야 한다. valueOf() 메소드를 정의할 때는 연산자의 동작을 바꾸는 것이 아니라 연산자의 기본 동작과 함께 사용할 값을 바꿔야 한다는 사실을 명심해야 한다.

toString()

toString() 메소드는 valueOf()가 원시 값이 아닌 참조 값을 반환할 때 대비책으로 호출된다. 또한 원시 값을 사용하는 중 문자열이 필요한 동작을 실행할 때 묵시적으로 호출되기도 한다. 예를 들어 더하기 연산자들 문자열과 함께 사용하면 다른 피연산자는 문자열로 자동 변환된다. 이때 다른 피연산자가 원시 값이라면 이 값은 자동으로 문자열 형태로 변환된다(예를 들어 true가 "true"로 바뀐다). 하지만 참조 값이라면 valueOf()가 먼저 호출된다. valueOf()에서도 참조 값을 반환하면 toString()이 호출되고 반환된 값이 사용된다. 다음 예제를 보자.

let book = {
 title: "객체지향 자바스크립트" 
};

let message = "Book = " + book;
console.log(message);  // "Book = [object Object]"

이 코드는 문자열 "Book = " 과 객체 book을 조립해 문자열을 작성한다. book은 객체이므로 toString() 메소드가 호출된다. 이 메소드는 Object.prototype에서 상속받은 것이며 대부분의 자바스크립트 엔진에서는 "[object Object]"를 반환한다.
이 값을 그대로 사용할 생각이라면 굳이 toString() 메소드를 직접 정의하지 않아도 상관없다. 하지만 때로는 직접 toStirng() 메소드를 작성하여 반환하는 문자열에서 더 많은 정보를 보여주고 싶을 때가 있다. 예를 들어 앞서 보았던 스크립트를 조금 수정해 책의 제목을 출력한다고 생각해보자.

let book = {
 title: "객체지향 자바스크립트",
 toString: function() {
  return  "[Book " + this.title + "]"
 }
};

let message = "Book = " + book;

// "Book = [Book 객체지향 자바스크립트]"
console.log(message); 

이 코드는 상속받은 것보다 더 유용한 정보를 반환하는 toString() 메소드를 book 객체에 정의한다. 보통은 toString() 메소드를 작성할 필요가 없지만 필요할 때의 이렇게 사용할 수 있다는 것 정도는 알아두면 좋다.

Object.prototype 수정

모든 객체는 기본적으로 Object.prototype을 상속받으므로 Object.prototype을 수정하면 모든 객체에 반영된다. 그런데 이 같은 상황은 매우 위험하다. 앞선 글에서도 내장 객체의 프로토타입을 수정하지 말라고 경고한 바 있는데, Object.prototype에는 이 경고를 두 배쯤 더 강하게 강조하고자 한다. 다음 코드를 살펴보자.

Object.prototype.add = function(value) {
 return this + value; 
};

let book = {
 title: "객체지향 자바스크립트" 
};

console.log(book.add(5));  // "[object Object]5"
console.log("title".add("end")));  // "titleend"

// 웹 브라우저에서 실행할 때
console.log(document.add(true));  // "[object HTMLDocument]true"
console.log(window.add(5));  // "[object Window]true"

Object.prototype.add()를 추가하면 모든 객체에 add() 메소드가 추가되는데, 현재 객체에 이 메소드가 적절하든 그렇지 않든 상관없이 무조건 추가된다. 이는 개발자뿐 아니라 자바스크립트 언어를 만드는 사람들에게도 문제가 된다. Object.prototype에 메소드를 추가하면 예상치 못한 문제가 생길 수 있기 때문에 새 메소드는 여러 곳에 나누어 추가해야 한다.

또 다른 문제는 열거 가능한 프로퍼티를 Object.prototype에 추가할 때 발생한다. 앞의 예제에서 Object.prototype.add()는 열거 가능한 프로퍼티인데, 다시 말해 for-in 반복문을 실행할 때 다음과 같이 나타날 수 있다는 뜻이다.

let empty = {};

for (let property in empty) {
	console.log(property);
}

이 코드에서 empty 객체의 프로토타입에는 add 프로퍼티가 있고 이 프로퍼티는 열거 가능하므로 "add"가 출력될 것이다. for-in 구조가 자바스크립트에서 종종 사용된다는 것을 감안하면 Object.prototype에 열거 가능한 프로퍼티를 추가하는 것은 지나치게 많은 코드에 영향을 끼칠 수 있다. 이 때문에 자바스크립트 언어의 개발에 참여한 더글라스 크락포드는 for-in 반복문은 다음과 같이 항상 hasOwnProperty()와 함께 사용할 것을 권장했다.

let empty = {};

for (let property in empty) {
  if(empty.hasOwnProperty(property)) {
	console.log(property);    
  }
} 

이 방식은 의도하지 않은 프로토타입 프로퍼티를 걸러내기에는 좋지만 for-in 반복문 안에서는 고유 프로퍼티만 사용할 수 있다는 단점도 함께 존재한다. 따라서 Object.prototype을 수정하지 않는 것이 가장 안전하고 확실한 방법이다.

객체 상속

가장 단순한 형태의 상속은 객체 간에 이루어진다. 새 객체의 [[Prototype]]으로 사용할 객체만 설정해주면 상속이 이루어진다. 객체 리터럴은 기본적으로 Object.prototype을 [[Prototype]]으로 설정하지만 Object.create() 메소드를 사용해 [[Prototype]]을 명시적으로 정해줄 수 있다.

Obejct.create() 메소드에는 인수 두 개를 전달하는데 첫 번째 인수로는 새 객체의 [[Prototype]]으로 사용할 객체를 전달하고 두 번째 인수에는 Object.defineProperties()에서 사용할 프로퍼티 서술자 객체를 전달한다. 두 번째 인수는 생략이 가능하다. 다음 코드를 살펴보자.

let book = {
 title: "객체지향 자바스크립트" 
};

// 위 코드는 아래와 같다.
let book = Object.create(Object.prototype, {
 				title: {
                  configurable: true,
                  enumerable: true,
                  value: "객체지향 자바스크립트",
                  writable: true
                }
});

이 코드에서 사용한 두 가지 방식의 실행 결과는 같다. 첫 번째 방식은 객체 리터럴을 사용해 title이라는 프로퍼티를 가진 객체를 정의한다. 이 객체는 자동으로 Object.prototype을 상속하고 title 프로퍼티를 기본 값인 설정 가능, 열거 가능, 쓰기 가능한 상태로 설정한다. 두 번째 방식 역시 같은 단계를 따르고 있지만 Object.create()를 명시적으로 사용한다. 각 단계를 거친 후 만들어진 book 객체는 첫 번째 방식에서 만든 것과 완전히 똑같이 동작한다. 하지만 Object.prototype은 굳이 이렇게 사용하지 않아도 자동으로 상속되기 때문에 아마 이런 식으로 Object.prototype을 상속하도록 명시적으로 작성할 일은 없을 것이다. Object.create()는 다른 객체를 상속받을 때 훨씬 더 유용하다.

let person1 = {
 name: "Nicholas",
 sayName: function() {
  console.log(this.name); 
 }
};

let person2 = Object.create(person1, {
 name: {
   configurable: true,
   enumerable: true,
   value: "Greg",
   writable: true
 }
});

person1.sayName();  // "Nicholas" 출력
person2.sayName();  // "Greg" 출력

console.log(person1.hasOwnProperty("sayName"));  // true
console.log(person1.isPrototypeOf(person2));  // true
console.log(person2.hasOwnProperty("sayName"));  // false

이 코드는 name 프로퍼티와 sayName() 메소드가 있는 person1 객체를 생성한다. person2 객체는 person1 객체를 상속받으므로 person2는 person1에서 name과 sayName()을 상속받는다. Object.create()를 사용해 person2를 생성하면서 name 프로퍼티를 다시 정의했다. 고유 프로퍼티는 같은 이름을 가진 프로토타입 프로퍼티를 가리므로 실제로 사용되는 것은 고유 프로퍼티이다. 따라서 person1.sayName()은 "Nicholas"를 출력하지만 person2.sayName()은 "Greg"을 출력하게 된다. sayName()은 person1에 정의되어 있으며 person2에도 상속되었다는 사실을 기억하자.

이 예제의 상속 체인은 person1보다 person2에서 더 길어진다. person2 객체는 person1 객체를 상속받았고 person1 객체는 Object.prototype을 상속받는다.

person2의 프로포타입 체인에는 person1과 Object.prototype이 연결되어 있다.

객체의 프로퍼티에 접근할 때 자바스크립트 엔진은 검색 과정을 거친다. 프로퍼티가 처음 접근한 인스턴스에 있으면(즉, 고유 프로퍼티라면) 그 프로퍼티의 값이 사용된다. 프로퍼티가 해당 인스턴스에 없으면 이어서 [[Prototype]]을 탐색한다. 여기서도 프로퍼티를 못 찾으면 인스턴스의 [[Prototype]]의 [[Prototype]]에서 계속 검색하고 이 과정을 체인이 끝날 때까지 계속한다. 보통 체인의 가장 마지막은 Object.prototype인데 이 객체의 [[Prototype]]은 null이다.

Object.create()를 사용하면 [[Prototype]]이 null인 객체도 다음과 같이 생성할 수 있다.

let nakedObject = Object.create(null);

console.log("toString" in nakedObject); // false
console.log("valueOf" in nakedObject); // false

이 예제의 nakedObject는 프로토타입 체인이 없는 객체이다. 다시 말해 이 객체에 toString()이나 valueOf()와 같은 내장 메소드가 존재하지 않는다는 뜻이다. 이 객체는 미리 정의된 프로퍼티가 하나도 없는 완벽한 백지 상태이다. 이런 객체를 사용해야 할 때가 많지는 않으며 Object.prototype에서 상속받아야 하는 기능은 하나도 사용할 수 없다. 예를 들어 nakedObject를 연산자와 함께 사용하면 "객체를 원시 값으로 변환할 수 없다" 라는 에러가 발생한다. 그렇다 해도 프로토타입이 없는 객체를 만들 수 있다는 점은 자바스크립트라는 언어의 재미있는 특성이다.

생성자 상속

자바스크립트의 객체 상속은 생성자 상속의 기반도 된다. 이전에 살펴본 것들을 기억해 보면 거의 모든 함수에는 prototype 프로퍼티가 있으며 이 프로퍼티는 수정하거나 교체될 수 없다고 했었다. prototype 프로퍼티는 따로 설정하지 않으면 기본적으로 Object.prototype을 상속받는 일반 객체 인스턴스가 되며 이 인스턴스는 constructor라는 고유 프로퍼티를 가지고 있다. 자바스크립트 엔진이 실제로 하는 일은 다음과 같다.

// 우리가 작성한 코드
function YourConstructor() {
 // initialization 
}

// 자바스크립트 엔진이 내부적으로 하는 일
YourConstructor.prototype = Object.create(Object.prototype, {
 					 			constructor: {
                                   configurable: true,
  								   enumerable: true,
								   value: YourConstructor,
   							       writable: true
                                }
							});

따라서 이 코드만 실행했다면 생성자의 prototype 프로퍼티는 Object.prototype을 상속받는 객체가 된다. 즉, YourConstructor의 인스턴스는 Object.prototype을 상속받으므로 YourConstructor는 Object의 하위타입(subtype)이고 Object는 YourConstructor의 상위타입(supertype)이 된다.

prototype 프로퍼티는 재정의할 수 있으므로 이 프로퍼티를 다시 정의하면 프로토타입 체인을 바꿀 수 있다. 다음 예제를 보자.

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

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

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

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

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

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

let rect = new Rectangle(5, 10);
let 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

이 코드에서 Rectangle과 Square라는 두 개의 생성자가 있다. Square 생성자의 prototype 프로퍼티는 Rectangle의 인스턴스가 설정되어 있다. 여기까지 설정할 때는 인수가 필요 없으므로 Rectangle에는 아무런 인수도 전달하지 않았다. 만약 이때 인수를 사용했다면 모든 Square 인스턴스가 같은 크기로 설정되었을 것이다. 이 방식으로 프로토타입 체인을 변경할 때는 아무런 인수가 전달되지 않아도 에러가 발생하지 않도록 생성자를 주의해서 작성해야 하며(인수가 반드시 전달되어야 하는 생성자를 많이 작성한다), 생성자는 어떤 종류의 전역 상태이든 수정하지 않아야 한다. 전역 변수 등을 사용해 생성된 인스턴스 개수를 추적하는 것이 한 예가 될 수 있다. constructor 프로퍼티는 Square.prototype을 덮어쓴 후에 원래 값으로 돌려놓아야 한다.

이 과정이 끝난 후에는 Rectangle의 인스턴스인 rect와 Square의 인스턴스인 square을 생성한다. 두 객체에는 Rectangle.prototype에서 상속받은 getArea()라는 메소드가 존재한다. square 변수는 Square의 인스턴스이자 Rectangle과 Object의 인스턴스이기도 하다. instanceof 연산자는 프로포타입 체인을 사용해 객체의 타입을 확인하기 때문에 이 변수는 세 가지 타입의 인스턴스로 나타난다.

square와 rect의 프로토타입 체인은 두 객체 모두 Rectangle.prototype과 Object.prototype을 상속받고 있다. square는 여기에 더해 Square.prototype도 상속받는다.

사실 Square.prototype은 굳이 Rectangle 객체로 다시 정의하지 않아도 되지만 Square에 불필요한 동작을 Rectangle 생성자에서 하는 것도 아니다. Square.prototype과 Rectangle.prototype은 단지 상속이 어떻게 일어나는지 보여주기 위해 연결했을 뿐이다. 따라서 Object.create()를 사용하면 이 예제를 더 단순하게 만들 수 있다.

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 프로퍼티도 잘 복원되어 있다.

프로토타입에 프로퍼티를 추가하는 것보다, 먼저 프로토타입 재정의가 이루어져야 한다. 그렇지 않으면 프로토타입을 다시 정의할 때 추가했던 메소드가 사라진다.

생성자 훔치기

자바스크립트에서 상속은 프로토타입을 통해 이주어지기 때문에 객체의 상위타입 생성자를 호출하지 않아도 된다. 상위타입의 생성자를 하위타입의 생성자에서 호출하려면 자바스크립트의 동작 방식에 대해 이해하고 이를 활용해야 한다.

이전에 우리는 함수를 호출할 때 다른 this 값을 사용하도록 만드는 call()과 apply() 메소드에 대해 배웠다. 생성자 훔치기(constructor stealing)가 하는 일도 이와 똑같다. 하위타입 생성자에서 call()이나 apply()를 사용하여 새로 생성된 객체를 인수로 전달하면 상위타입 생성자를 호출할 수 있다. 다음 예제는 직접 작성한 객체에서 상위타입 생성자를 훔치는 코드이다.

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 + "x" + 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 + "]";
};

let square = new Square(6);

console.log(square.length);  // 6
console.log(square.width);  // 6
console.log(square.getArea());  // 36

Square 생성자는 Rectangel 생성자를 호출하면서 this와 동시에 size를 두 차례 인수로 사용했다(하나는 length에 다른 하나는 width에 저장된다). 따라서 length와 width 프로퍼티의 값이 size와 같은 객체가 새로 생성된다. 이 방법을 사용하면 생성자에서 정의한 프로퍼티를 다시 정의하는 것을 방지할 수 있다. 상위타입 생성자를 호출한 뒤에 새 프로퍼티를 추가하거나 기존 프로퍼티를 수정하면 된다.

생성자를 이렇게 두 단계롤 나누어 실행하면 직접 만든 두 객체 간에 상속이 이루어질 때 매우 유용하다. 생성자의 프로토타입을 수정할 일은 늘 있겠지만 하위타입 생성자 안에서 상위타입 생성자도 함께 호출해야 할 수도 있다. 일반적으로 프로토타입 상속은 메소드 상속을 위해 사용하고 생성자 훔치기는 프로퍼티 상속을 위해 사용한다. 이 방식은 클래스 기반 언어의 클래스 상속을 흉내 냈기 때문에 이를 가리켜 의사 클래스 상속(pseudoclassical inheritance)이라고 부른다.

상위타입 메소드 접근

앞 예제에서 Square 타입은 고유한 toString() 메소드를 가지고 있으며 이 메소드는 프로토타입에 있는 toString()을 가린다. 이처럼 하위타입의 새 기능으로 상위타입의 메소드를 덮어쓰는 일은 매우 흔히 일어난다. 하지만 이때 가려진 상위타입의 메소드에 접근하고 싶다면? 다른 언어에서는 super.toStirng()을 통해 가능했겠지만 자바스크립트에는 이와 비슷한 기능이 존재하지 않는다. 하지만 상위타입의 프로토타입에 있는 메소드에 바로 접근할 수 있으며 call()이나 apply()를 사용하면 이 메소드를 마치 하위타입의 메소드인양 호출할 수도 있다. 다음 예제를 보자.

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 + "x" + 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() {
 let text = Rectangle.prototype.toString.call(this);
 return text.replace("Rectangle", "Square");
};

이 코드에서 Square.prototype.toString()은 call()을 사용해 Rectangle.prototype.toString()을 호출한다. 메소드를 실행한 후 수정해야 할 부분은 값을 반환하기 전에 "Rectangle"을 "Square"로 바꾸는 것뿐이다. 예제와 같은 간단한 동작에서는 이 방식이 조금 번잡해 보일 수 있지만 이것만이 상위타입의 메소드에 접근할 수 있는 유일한 방법이다.

요약

자바스크립트는 프로토타입 체이닝을 통해 상속을 지원한다. 프로토타입 체인은 객체 간에 이루어지는데 한 객체의 [[Prototype]] 프로퍼티가 다른 객체로 설정될 때 일어난다. 모든 일반 객체는 Object.prototype을 자동으로 상속한다. 다른 객체를 상속하는 객체를 만들고 싶다면 Object.crate()를 사용해 새 객체의 [[Prototype]]으로 사용할 값을 정해주면 된다.

생성자에 프로토타입 체인(prototype chain)을 만들면 두 타입 간의 상속을 할 수 있다. 생성자의 prototype 프로퍼티를 다른 값으로 설정하는 것은 곧 이 생성자의 인스턴스와 다른 값의 프로토타입 사이에 상속 관계를 만드는 것이다. 같은 생성자로 만들어진 인스턴스는 모두 같은 프로토타입을 공유하기 때문에 인스턴스는 모두 같은 객체를 상속한다. 이 기법은 다른 객체에서 메소드를 상속받을 때는 매우 잘 동작하지만 프로토타입만 사용해서는 고유 프로퍼티를 상속받을 수 없다.

고유 프로퍼티를 제대로 상속받을 때는 하위타입 객체에서 call()이나 apply()를 사용해 상위타입 객체의 생성자를 호출하는 생성자 훔치기(constructor stealing)를 사용할 수 있다. 자바스크립트의 상속은 대부분 생성자 훔치기와 프로토타입 체인을 함께 사용하여 이루어진다. 이 조합은 클래스 기반 언어의 상속을 비슷하게 흉내 낸 것이기 때문에 의사 클래스 상속이라고도 부른다.

상위타입의 프로토타입을 사용하면 상위 타입의 메소드에 바로 접근할 수 있다. 이 방법을 사용할 때는 상위타입 메소드에 call()이나 apply()를 사용하여 마치 하위 타입 객체에서 실행된 것처럼 사용해야 한다.

profile
I'm on Wave, I'm on the Vibe.

0개의 댓글