자바스크립트에서 객체를 생성할 때 크게 두 가지 방법을 사용한다. 그때의 상속 방법을 비교해 보자.
ES5 기준으로 생성자 함수로 객체를 생성할 때 pseudoclassical inheritance 방식을 이용한다.
pseudoclassical inheritance 방식은 자바나 C에서 온 사람들이 친숙한 방식으로 상속을 하려고 한다.
pseudoclassical inheritance을 이용하여 클래스 상속을 사용하고 객체가 해당 클래스의 인스턴스인 위치를 사용하여 클래식 프로그래밍 언어의 동작을 재현하려고 시도한다.
우리가 상속을 시켜줄 때 prototype을 사용했다. 그렇다면 부모 생성자 함수의 프로토 타입을 자식 생성자 함수의 프로토 타입으로 설정해 주면 되지 않을까?
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
this.name = name;
this.age = age;
}
Child.prototype = Parent.prototype;
const parent = new Parent("jane", 35);
parent.sayName(); // jane
const child = new Child("john", 2);
child.sayName(); // john
물론 이 방식도 완벽히 틀렸다고 말할 수는 없다. 하지만 자식 프로토타입에 부모 프로토타입의 참조를 넣었으니 자식 프로토타입에서 개별적인 추가나 수정이 불가하다.
아래의 코드에서 자식 프로토타입을 추가했을 경우 부모까지 프로토타입까지 영향을 받는 상황을 볼 수 있다.
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
this.name = name;
this.age = age;
}
Child.prototype = Parent.prototype;
Child.prototype.sleep = function() {
console.log("....zzZ");
};
const parent = new Parent("jane", 35);
parent.sayName(); // jane
parent.sleep(); // ....zzZ
const child = new Child("john", 2);
child.sayName(); // john
child.sayName(); // ....zzZ
따라서 우리는 조금의 수정이 필요하다. 자식의 프로토타입에 부모의 프로토타입을 가지고 있는 객체를 넣어주면 되지 않을까? 적용해보자..!
Child.prototype = { ...Parent.prototype };
Child.prototype.sleep = function() {
console.log("....zzZ");
};
const parent = new Parent("jane", 35);
parent.sayName(); // jane
parent.sleep(); // TypeError: parent.sleep is not a function
const child = new Child("john", 2);
child.sayName(); // john
child.sleep(); // ....zzZ
spread 연산자를 이용해 새로운 객체에 부모 프로토타입을 넣어준다.
그러면 새로운 객체가 생겨 부모 프로토타입을 참조하지 않는다. 단, 이 해결 방법은 또 다른 문제점을 가져온다.
이 상태에서 부모 프로토타입에 메서드를 추가한다면? 참조가 끊겨 자식 프로토타입에 전달이 되지 않을 것이다.
Child.prototype = { ...Parent.prototype };
Child.prototype.sleep = function() {
console.log("....zzZ");
};
Parent.prototype.sayHello = function() {
console.log("hello");
};
const parent = new Parent("jane", 35);
parent.sayName(); // jane
parent.sayHello(); // hello
const child = new Child("john", 2);
child.sayName(); // john
child.sleep(); // ....zzZ
child.sayHello(); // TypeError: parent.sleep is not a function
그렇다면 우리는 어떤 방식을 써서 상속을 시켜줘야 할까?
바로 Object.create(proto) 메서드를 사용하는 것이다. 메서드의 첫 번째 인자값 proto에는 새로 만든 객체의 프로토타입이어야 할 객체를 넣어준다. 따라서 이 방식을 이용해 수정을 해본다면 아래와 같다.
Child.prototype = Object.create(Parent.prototype);
Child.prototype.sleep = function() {
console.log("....zzZ");
};
Parent.prototype.sayHello = function() {
console.log("hello");
};
const parent = new Parent("jane", 35);
parent.sayName(); // jane
parent.sayHello(); // hello
const child = new Child("john", 2);
child.sayName(); // john
child.sleep(); // ....zzZ
child.sayHello(); // hello
잘 해결된 모습이 보인다. 하지만 또 한 가지 의문이 생긴다. 프로토 타입이 잘 들어가긴 했는데 그 안에 들어가 있는 constructor도 변경되지 않았을까? 직접 확인해 보자.
console.log(child.__proto__.constructor); // function Parent() {}
우리가 child.__proto__.constructor 으로 기대한 건 Parent가 아니라 Child이다.
따라서 완벽한 상속 관계를 만들어 주기 위해서 수정을 해줘야 한다.
Child.prototype.constructor = Child;
console.log(child.__proto__.constructor); // function Child() {}
역시 끝이 아니다. 🤨 🤨
부모에게서 상속 받을속성이 있다면 부모.call(this) 또는 부모.apply(this,arguments)를 사용해 상속을 받아 그대로 사용하거나 또는 새로운 값을 할당할 수 있다.
// Case1
function Child(name, age) {
// ~~~~
}
const child = new Child("john", 2);
child.sayName(); // undefined
// ---------------------------------
// Case2
function Child(name, age) {
Parent.apply(this, arguments);
}
const child = new Child("john", 2);
child.sayName(); // john
클래스는 ES6 기준으로 추가된 문법이다. 기존의 프로토타입 기반 상속보다 명료하게 사용이 가능하다.
다만 자바스크립트에서 class는 syntactic sugar이다.
새로운 객체지향 상속 모델을 제공하는 게 아니라 좀 더 단순하고 명확한 문법만 제공해 주는 것이다.
클래스를 이용해서 상속을 할 경우 굉장히 간단하다.
클래스에 생성자 함수를 만들어 주고 속성 값을 세팅해 준다. 또 클래스에 함수를 넣어 메서드를 바로 프로토타입에 추가할 수 있다.
자식 클래스를 만들 때는 extends라는 키워드를 사용해 상속을 받을 수 있다.
또한 부모 클래스에서 pseudoclassical inheritance 방식에서 부모 속성 값을 상속받아 사용하려면
call, apply를 사용해야 하는데 class에서는 super라는 키워드를 사용하면 쉽게 상속받아 사용이 가능하다.
그럼 직접 위의 pseudoclassical inheritance 방식 코드를 클래스 방식으로 수정해 보자.
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
sayHello() {
console.log("Hello!");
}
}
class Child extends Parent {
constructor(name, age) {
super(name, age);
}
sleep() {
console.log("....zzZ");
}
}
const parent = new Parent("jane", 30);
const child = new Child("john", 2);
parent.sayName(); // jane
parent.sayHello(); // Hello!
parent.sleep(); // TypeError: parent.sleep is not a function
child.sayName(); // john
child.sayHello(); //Hello!
child.sleep(); // ....zzZ
부모 클래스에서 상속받은 속성을 그대로 사용하고 싶다면 자식 클래스에서는 constructor를 생략을 해도 된다.
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
sayHello() {
console.log("Hello!");
}
}
class Child extends Parent {
sleep() {
console.log("....zzZ");
}
}
const parent = new Parent("jane", 30);
const child = new Child("john", 2);
parent.sayName(); // jane
parent.sayHello(); // Hello!
parent.sleep(); // TypeError: parent.sleep is not a function
child.sayName(); // john
child.sayHello(); //Hello!
child.sleep(); // ....zzZ
당연히 class만 알아도 되는 거 아닌가라는 의문이 생길 수 있다.
pseudoclassical inheritance에서는 코드의 길이도 그렇고 신경 써줘야 하는 부분이 너무 많다.
하지만 우리가 앞으로 만날 코드가 모두 클래스로 만들어져 있을지 장담하지 못한다. 따라서 둘 다 알아야 하는 상황인 것이다.
또한 객체 지향 프로그래밍에서 상속을 이해하기 위한 방법으로는 오히려 pseudoclassical inheritance 방식이 더 유리하다.
나 또한 이 방식을 비교하면서 상속이 어떻게 되는지 좀 더 확실히 알게 된 거 같다.