[JavaScript] 객체지향언어 (prototype, inheritance, super, __proto__)

객체 지향 프로그래밍이란 객체를 사용하여 개체를 표현하는 방식을 객체 지향 프로그래밍(OOP)라고 말한다.

객체란 같은 취지의 서로 연관된 변수와 함수들을 grouping 하여 이름을 붙인것이다.

prototype

프로토타입은 생성자함수와같이 재사용성을 높히기위한것이다.

function Person(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
}

Person.prototype.sum = function () {
return "prototype : " + (this.first + this.second);
};

let kim = new Person("kim", 10, 20);
kim.sum = function () {
return "this : " + (this.first + this.second);
};

let lee = new Person("lee", 10, 10);

console.log(kim.sum()); //this : 30
console.log(lee.sum()); //prototype : 20



일단 코드가 동작하는 순서를 설명해보면 맨밑에 console.log를 통해 kim과 lee객체에있는 sum()함수를 호출한다. 이 경우 생성자 함수안에 sum()함수를 일단 찾아보고 없는경우 해당 객체의 생성자인 Person의 prototype에 정의되어있는 sum()메소드를 통해 값을 가져온다.

하지만 kim의 경우 kim객체안에 따로 sum()함수를 지정해주었기 때문에 따로 prototype을 참조하지않고 자신이 소유하고있는 sum()함수를 실행한다. 그러므로 kim객체의 경우 결과값으로 30을 반환하며 lee객체의 경우 20을 반환한다.

*프로토타입을 사용하는 이유는 객체가 생성될때마다 해당 객체의 메소드를 만들게되면 메모리를 할당해야하기 때문에  많은 메모리를 차지하게되는데, 그렇게 하지않고 프로토타입을  한번만 정의함으로서 모든 객체가 공통적으로 프로토타입을 참조하여 사용할 수 있게 되고 이로인해 메모리를 효율적으로 사용할 수 있도록하는 장점이있으며 메소드의 재정의가 필요한경우 kim에게 따로 메소드를 지정해준 비슷한방법으로 재정의하여 상황에 맞게 자신만이 사용가능한 메소드를 재정의 할 수 있어 유지보수에도 많은 도움이됩니다. 

+Object.constructor를 사용하여 객체가 어느생성자함수를통해 만들어졌는지 출신을 알 수 있다.
(객체명.constructor)

*******하지만 이와같은 객체의 확장은 문제를 야기할 수 있다.*********

아래의 내용을보고 주석을 참고자하자

Object.prototype.contain = function(obj){
for(var key in this){ //여기에서의 this는 contain프로퍼티를 사용한 객체를 가리킨다.
if(this[key] === obj){
return true;
}
}
return false;
}


var o = {name:'joonho', city:'seoul'};
console.log(o.contain('joonho')) //true
var a = ['joonho', 'para','site'];
console.log(a.contain('kim')) //false


for(var name in o){
console.log(name); // 결과값 name, city, contain
}
// Object객체 확장을 통해 프로퍼티를 상속할경우 자신이 원하는 객체의 값만 추출하는데 문제가 발생하게된다.


for(var name in o){
if(o.hasOwnProperty(name)){
console.log(name); // 결과값 name, city
}
}
// 상속으로인한 문제점을 해결하기위해 hasOwnProperty룰 사용하면 상속받은 객체를 제외한 자신만이 직접적으로
소유하고있는 객체를 추출할 수 있는 메소드이다. 이렇게되면 상속받은 프로퍼티와 직접적으로 입력한 프로퍼티값을 구분하여
추출할 수 있다. 위에서 객체 확장으로인해 발생된 객체자신의 값이외에 상속받은 즉 확장객체 프로퍼티까지 추출되는 문제점을
해결할 수 있다.


간단히 요약하자면 객체를 확장하게되면 객체의 프로퍼티값을 추출할때 추출하려고 하지않은 확장된 프로퍼티까지 추출하게되는 문제점을 발생시킬 수 있다. 그로므로 조심해야되고 이러한 문제점을 해결하기위해서 hasOwnProperty를 사용하면 확장된 객체 프로퍼티를 제외한 객체의 자신만이 가지고 있는 고유의 프로퍼티만을 추출할 수 있다.


class

constructor funtion(생성자 함수)와같이 객체를 생성하는 공장역할을하며 조금 더 가독성을 높여주고 코드의 양을 줄여준다.

class Person {
constructor(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
}
sum() {
return "prototype : " + (this.first + this.second);
}
}

let kim = new Person("kim", 10, 20);
kim.sum = function () {
return "this : " + (this.first + this.second);
};

let lee = new Person("lee", 10, 10);

console.log(kim.sum()); //this : 30
console.log(lee.sum()); //prototype : 20

생성자 함수를대신해서 class라는 방법으로 객체공장을 생성한다. 이전에 설명한 생성자함수와 다르게 class에 찍어낼 객체의 이름을 적어주고 그안에 constructor라는함수를 사용하여 매개변수와 속성을 지정해준다. constructor의 이름을 아무렇게나 바꾸면 안되는데 그 이유는 자바스크립트에서 객체를 생성할때 constructor함수의 속성들을 참조하도록 약속되어있는 언어이기 때문이다.
그리고 프로토타입속성을 사용하지않고 위와같이 class Person안에 constructor함수의 속성들과 같이 작성하지 않고 따로 sum()메소드를 작성하면 sum()메소드는  Person.prototype.sum = function(){}과 동일하게 동작한다.
또한 sum()메소드를 참조하지않고 새로생성된 객체에 다른 메소드 속성을 부여하고 싶을때 16, 17번 열 코드처럼 작성하여 따로 메소드 속성을 지정해줄 수 있다.

*class 옆에 객체명을 작성하고 그 안에 constructor를 필수로 적어줘야한다.
*위에 코드를보면 Person은 다른언어에서처럼 클래스로 보이지만 console.log(toString.call(Person));을 해보면 [object Function] 이렇게 결과가나오는데 함수라는것을 말하고있다. 겉보기에는 class이지만 결국 fuction과 prototype으로 연결된 구조이다.


inheritance(상속)

상속이란 코드의 중복을 제거하기 위한 방법이며 기존에 기능들을 수행하며 존재하던 코드에 추가적인 기능들을 추가해주고싶을때 사용한다. 또한 먼저 생성되어 상속해주는 class만 바꾸어도 확장된 class까지 동시다발적으로 기능을 바꿀 수 있어 유지보수에 효율적이다.

class Person {
constructor(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
}
sum() {
return this.first + this.second;
}
}

class PersonPlus extends Person {
// constructor(name, first, second) {
// this.name = name;
// this.first = first;
// this.second = second;
// }
// sum() {
// return (this.first + this.second);
// }
avg() {
return (this.first + this.second) / 2;
}
}

let kim = new PersonPlus("kim", 10, 20);
console.log(kim.sum()); //30
console.log(kim.avg()); //15


위의 코드에서 주석처리 된부분만큼의 중복이 제거되면서도 extends를 사용하여 class Person의 속성들을 상속받기 때문에 똑같은 효과를 class PersonPlus에서도 사용할 수 있으며 class PersonPlus에서는 추가적으로 avg()메서드도 사용할 수 있다.

*기존에 존재하던 class Person에 메서드를 추가적으로 작성하여도 되지만 블로그에 예시로 작성된 코드처럼 간단한것이아닌 라이브러리를 가져오거나 내가 작성하지않고 남이 작성한 코드를 수정하는건 여러가지 에러들을 발생시킬 수 있다. 그러므로 될 수 있다면 직접적으로 추가하기보다는 새로운 클래스를 생성하여 위와같이 메서드를 추가해주는것을 권장한다.

*상속받는 class를 자식class, 상속해주는 class를 부모class라고 지칭한다. 또는
상속받는 class를 sub class, 상속해주는 class를 super class라고 지칭한다.
위의 코드에서 class Person은 부모class, class PersonPlus는 자식class이다.


super

상속을 사용하는 이유는 기존의 로직(생성자함수 속성들)에 추가적인 속성을 지정해주고 싶을때 사용한다. 기존의 로직에  직접적으로 수정하는 작업을 통해서도 수정이 가능하나 내가 만든로직이아닌 타인이 만들었거나 혹은 다른사람이 만든 라이브러리등일경우 수정시 오류를 발생시킬 가능성이 크기때문에 기존의 로직에 수정작업을 하기 어렵다. 이런경우 상속을통해 기존속성을 상속받고 수정하고자하는 작업을 추가할 수 있다.

부모class를 건드리지 않고 상속을 통해 기존의 로직에 원하는 작업을 추가하는 방식으로 자식class를 개선할 수 있다. 이때 사용하는게 super라는 키워드이다. super 자체는 부모 class를 의미한다. 이를통해 문제를 발생시킬 확률을 줄일뿐더러 코드의 중복또한 줄일 수 있다.

class Person {
constructor(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
}
sum() {
return this.first + this.second;
}
}

class PersonPlus extends Person {
constructor(name, first, second, third) {
super(name, first, second);
this.third = third;
}
sum() {
return super.sum() + this.third;
}
avg() {
return (this.first + this.second + this.third) / 3;
}
}

let kim = new PersonPlus("kim", 10, 20, 30);
console.log(kim.sum()); //60
console.log(kim.avg()); //20


기존 코드에 this.third = thrid; 속성을 추가하고자 할때 위에있는 Person안에 생성자함수를 그대로 가져와 위의 부모class와 중복되는 코드는 super로 묶고 매개변수를 지정해준다. 이렇게 작성할경우 this.name, this.first, this.second와 같은 코드가 super라는 함수하나로 똑같은 역할을 하게된다.

속성이 추가되면서 그와관련된 함수들도 수정이 필요하게되는데 sum()메소드의 코드도 중복이기때문에 super.sum()을통해 부모class의 this.first+this.second 기능을 대체한다.

super()는 부모클래스의 생성자함수(constructor)를 부르는것이고 super.method()는 부모클래스에있는 메소드를 호출하는 것이다.



__proto__

__proto__ 를 사용하면 class를 사용하지않고도 어떠한 객체의 자식객체로 들어갈 수 있다.

let superObj = {superVal:'super'}
let subObj = {subVal:'sub'}
subObj.__proto__ = superObj; //superObj의 자식객체로 subObj가 들어갔다. (부모객체를 참조한다.)
console.log(subObj.subVal); //sub 1번
console.log(subObj.superVal); //super 2번

subObj.superVal ='sub';
console.log(subObj.superVal); //sub 3번
console.log(superObj.superVal); //super 4번
console.log(superObj.subVal); //undefined 5번

위의 코드를보면 subObj에 __proto__ 를 사용하여 superObj의 자식객체로 들어갔다. 이렇게 코드를 작성할 경우 자식객체는 부모객체에 접근할 수 있게된다. 작동 순서를 보면

1번 문장을 작성하면 객체의 프로퍼티가 그대로 반환되어 sub를 반환한다.

2번 문장을 작성하면 먼저 subObj 자신의 객체안의 속성에 superVal가 존재하는지 확인해보고 없는경우에 부모객체의 프로퍼티값을 받아 반환한다.

3번 문장을 작성하면 바로위의 코드처럼 정의되어있어 sub를 반환하는데 이것은 superObj객체의 프로퍼티값을 바꾼것이 아니라 subObj객체 자신의 프로퍼티에 superVal프로퍼티를 새롭게 할당한것이다. 확인을하기위해서

4번 문장을 작성하면 superObj.superVal는 여전히 프로퍼티로 super를 반환하는것을 확인할 수 있다.

5번 문장을 작성하면 undefined가 반환되는 그 이유는 부모객체는 자식객체의 프로퍼티를 참조할 수 없기 때문이다.


Object.create()

부모객체를 참조하는 자식객체 만들때 사용하는 코드이다. (  ) 안에 인자로 들어가는 객체가 부모객체가 된다. 불변성을 유지해야 할때도 사용한다.

let superObj = { superVal: "super" };
// let subObj = {subVal:'sub'}
// subObj.__proto__ = superObj;
let subObj = Object.create(superObj); //위에 두줄 주석처리한 코드와
subObj.subVal = "sub"; //같은 역할을 하는 코드이다.
console.log(subObj.subVal); //sub
console.log(subObj.superVal); //super

subObj.superVal = "sub";
console.log(subObj.superVal); //sub
console.log(superObj.superVal); //super
console.log(superObj.subVal); //undefined


코드를보면 let subObj = Object.create(superObj)의 의미는 superObj를 부모객체로하는 새로운 자식객체인 subObj를 만들라는 의미이다. 새로 만들어진 객체의 프로퍼티를 subObj.subVal = 'sub'; 로 선언하면 위에 주석처리 되어진 코드 두줄과 같은 역할을 하는 코드이다.

* __proto__를 사용하기보다는 Object.create()를 사용하는것을 권장한다. 두개는 부모객체와 자식객체를 연결해주는 역할은 똑같다
 

댓글