제대로 배우자, 자바스크립트(Javascript) #6 – 상속

자바스크립트는 클래스 같은 구조 관계를 정의할 수 없기 때문에 프로토타입을 통해 상속이라는 메커니즘을 구현한다.

I. 프로토타입 체이닝과 Object.prototype

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

직접 작성한 객체를 비롯해 모든 객체는 별도로 정하지 않으면 Object.prototype을 상속받는다. 객체 리터럴을 사용해 정의된 객체의 [[Prototype]] 프로퍼티는 모두 Object.prototype을 가리킨다.

II. Object.prototype에서 메소드 상속

아래 메소드는 Object.prototype에 정의되어 있으며 이는 기본적으로 모든 객체에 상속된다.

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

이 중 valueOf()와 toString()은 자바스크립트에서 객체를 일관성 있는 방식으로 다루기 위해 중요하며, 때로는 직접 작성해야 할 수 있다.

ValueOf() 메소드는 객체에 연산자를 사용할 때 호출된다. valueOf()는 객체 그 자체를 반환하는 것이 일반적이다. Date 객체의 valueOf() 멤소드는 밀리초 단위의 epoch 시간을 반환하도록 수정되어 있으며 결과는 Date.prototype.getTime()과 동일하다. 덕분에 날짜를 다음과 같이 비교할 수 있다.

var now = new Date();
var earlier = new Date(2017, 7, 20);

console.log(“time1: ” + now);
console.log(“time2: ” + earlier);

console.log(“time1’s epoch time: ” + now.valueOf());
console.log(“time2’s epoch time: ” + earlier.valueOf());

console.log(now > earlier);

toString() 메소드는 value()가 원시 값이 아닌 참조 값을 반환할 때 대비책으로 호출된다. 또한 원시 값을 사용하는 중 문자열이 필요한 동작을 실행할 때 묵시적으로 호출되기도 한다.

var book = {
title: “객체지향 자바스크립트의 원리”,
toString: function() {
return “[Book ” + this.title + “]”;
}
};

var message = “Book = ” + book;
console.log(message);

III. Object.prototype 수정

Object.prototype을 수정하면 모든 객체에 반영된다. 지나치게 많은 코드에 영향을 끼칠 수 있다. 수정하지마라.

IV. 객체 상속

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

Object.create() 메소드에는 인수 두 개를 전달한다. 첫 번째 인수에는 새 객체의 [[Prototype]]으로 사용할 객체를 전달하고, 두 번째 인수에는 Object.defineProperties()에서 사용할 프로퍼티 서술자 객체를 전달한다. 두 번째 인수는 생략할 수 있다.

var book = { title: “객체지향 자바스크립트의 원리” };

// 위 코드는 아래 코드와 동일하다.

 

var book = Object.create(Object.prototype, {
title: {
configurable: true,
enumerable: true,
value: “객제지향 자바스크립트의 원리”,
writable: true
}
});

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

Object.create()는 다른 객체를 상속받을 때 훨씬 더 유용하다.

var person1 = {
name: “Sung Am YANG”,
sayName: function() {
console.log(this.name);
}
};

var person2 = Object.create(person1, {
name: {
configurable: true,
enumerable: true,
value: “Ryan”,
writable: true
}
});

person1.sayName();
person2.sayName();

console.log(person1.hasOwnProperty(“sayName”));
console.log(person1.isPrototypeOf(person2));
console.log(person2.hasOwnProperty(“sayName”));

여기서 person2 객체는 person1 객체를 상속받으므로 person2는 person1에서 name과 sayName()을 상속받는다.
상속 구조

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

V. 생성자 상속

Prototype 프로퍼티는 따로 설정하지 않으면 기본적으로 Object.prototype을 상속받는 일반 객체 인스턴스가 되며 이 인스턴스는 constructor라는 고유 프로퍼티를 가지고 있다. 자바스크립트 엔진이 실제로 하는 일은 다음과 같다.

// 여러분이 작성한 코드
function YourConstructor() {}

// 자바스크립트 엔진이 내부적으로 하는 일
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)이 된다.

VI. 상위타입 메소드 접근

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.height + “]”;
};

function Square(size) {
Rectangle.call(this, size, size);
}

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

Square.prototype.toString = function() {
var text = Rectangle.prototype.toString.call(this);
return text.replace(“Rectangle”, “Square”);
};

이 방식이 조금 번잡해 보일 수 있지만 이것만이 상위타입의 메소드에 접근할 수 있는 유일한 방법이다.

VII. 요약

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

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

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

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

Leave a Reply

Your email address will not be published. Required fields are marked *