본문으로 바로가기

객체 확장

category JavaScript/JS 객체지향 프로그래밍 2016. 9. 29. 10:35

멤버 확장

확장이라 함은 부모 객체의 멤버를 모두 자식 객체로 전달하고 자식만의 멤버를 추가하는 것을 말합니다.

일반 객체지향 언어에서는 확장의 방법으로 대개 상속을 이용하는데 이 글에서 소개하는 방법은 실용적이면서도 간단한 자바스크립트만의 확장 방법에 대해 알아봅니다.

다음과 같은 순서로 상속을 구현하는 방법에 대해 소개합니다.

  • 프로토타입 멤버 상속
  • 인스턴스 멤버 상속
  • 멤버 확장





프로토타입 멤버 상속 구현 - prototype

여기서 소개하는 프로토타입 멤버 상속은 자바스크립트에서 흔히 이용되는 방법입니다.


Person을 상속하는 Korean을 정의해 보도록 합니다.

다음은 먼저 Person 생성자의 코드입니다.

javascript
function Person() {
    // 인스턴스 멤버 정의는 없음
}

// Person의 프로토타입 멤버를 정의
Person.prototype.species = 'human';


다음은 Korean 생성자를 정의해 봅니다.

Korean에서 nationality 프로토타입 멤버를 추가적으로 정의하도록 합니다.

javascript
// Korean 정의
function Korean() {
    // 인스턴스 멤버 정의는 없음
}
// Korean의 프로토타입 멤버를 정의
Korean.prototype.nationality = 'korea';


이제 Korean이 Person의 프로토타입 멤버를 상속하도록 코드를 작성해 봅니다.

javascript
Korean.prototye = new Person();
Korean.prototype.constructor = Korean;


프로토타입 멤버의 상속은 Person 객체를 정의할 때 내부적으로 다음과 같은 코드가 실행되어 Object 프로토타입 객체를 상속받습니다.

javascript
Person.prototype = new Object();
Person.prototype.constructor = Person

위의 코드는 Person의 프로토타입 객체로 Object 인스턴스를 사용하고 Person 생성자를 가리키는 constructor를 방금 추가한 프로토타입 객체의 멤버로 추가하는 작업입니다.

Object를 상속하는 Person을 정의할 때는 앞의 두 작업을 자바스크립트가 자동으로 알아서 처리해 줍니다.

그러나 여기서 하려는 것은 Object 가 아닌 Person을 상속받아 다른 사용자 정의 자식 타입을 정의하려고 하는 것입니다.

이 경우에는 자바스크립트가 자동으로 해줬던 내부 작업을 사용자가 직접 구현해야 합니다.


javascript
function Person() {
    // 인스턴스 멤버 정의는 없음
}

// Person의 프로토타입 멤버를 정의
Person.prototype.species = 'human';

// Korean 정의
function Korean() {
    // 인스턴스 멤버 정의는 없음
}
// Korean의 프로토타입 멤버를 정의
Korean.prototype.nationality = 'korea';

Korean.prototype = new Person();
Korean.prototype.constructor = Korean;

위 코드에서는 Korean 프로토타입 객체로 Person의 인스턴스를 사용하고 있고 Korean의 프로토타입 객체의 constructor 속성값을 Korean 생성자에 대한 참조로 변경했습니다.

이 코드는 결구 Korean 프로토타입 객체를 Person 인스턴스로 대체하는 작업을 하는 것으로서 하단의 두줄 코드가 실행되고 나면 Korean의 프로토타입 체인에 Person의 프로토타입 객체가 추가되는 결과가 나타나게 될 것입니다.


Person 프로토타입 멤버의 상속이란 결국 프로토타입 체인상에서 Korean의 프로토타입 객체의 상위에 Person의 프로토타입 객체를 만들어 끼워넣는 작업이다.


이제 Korean 인스턴스를 생성해서 사용하면 Korean에서 정의하지 않은 species를 사용할 수 있습니다.

다음은 프로토타입 멤버의 상속이 완성된 Korean의 인스턴스를 생성해서 사용하는 코드입니다.

javascript
function Person() {
    // 인스턴스 멤버 정의는 없음
}

// Person의 프로토타입 멤버를 정의
Person.prototype.species = 'human';

// Korean 정의
function Korean() {
    // 인스턴스 멤버 정의는 없음
}

Korean.prototype = new Person();
Korean.prototype.constructor = Korean;

// Korean의 프로토타입 멤버를 정의
Korean.prototype.nationality = 'korea';

var obj = new Korean();
var species = obj.species;
console.log(species); // human 반환
var nationality = obj.nationality;
console.log(nationality); // korea 반환


다음은 Korean 인스턴스에 대해 instanceof 연산의 결과를 보여주고 있습니다.

javascript
var p = new Korean();
console.log(p instanceof Korean); // true 반환
console.log(p instanceof Person); // true 반환


지금까지 본 프로토타입 멤버를 상속하는 방법을 다른 문서에서는 "프로토타입 상속"이라는 말로 표현하기도 합니다.




인스턴스 멤버의 상속 구현 - call/apply

자바스크립트 개발을 하다 보면 프로토타입 멤버뿐 아니라 부모 객체에 있는 인스턴스 멤버도 사용하고 싶을 때가 있습니다.

이제 부모에 정의된 인스턴스 멤버를 자식의 인스턴스에서도 사용할 수 있게 만드는 방법을 알아봅니다.


어떻게 부모 객체에 정의된 인스턴스 멤버를 자식 객체에서 사용하게 할 수 있을까?

일반 객체지향 언어에서는 base()super() 같은 메서드를 제공해 주고 있습니다. 그래서 자식 객체를 생성할 때 이것을 호출하게 해서 부모 객체도 생성하게 만들 수 있습니다.

일반 객체지향 언어에서는 부모 객체와 자식 객체가 별도로 생성되어서 자동으로 상속되는 구조입니다.

그러나 자바스크립트에서는 base()super() 같은 함수나 메서드를 제공하지 않습니다.


그럼 어떻게 인스턴스 상속을 구현할까?

지금 설명하려는 상황을 정리해 봅니다.

다음과 같이 Person에 인스턴스 멤버인 name이 정의되어 있다고 가정해 봅니다.

그리고 Korean에는 다음과 같이 city라는 인스턴스 속성이 정의해 봅니다.

javascript
function Person(name) {
    this.name = name; // 인스턴스 멤버 정의
}

function Korean(name, city) {
    this.city = city; // 인스턴스 멤버 정의
}



인스턴스 멤버 상속

위와 같은 상황에서 Korean으로 생성된 인스턴스에서 Person에 정의된 인스턴스 멤버인 name을 사용하고 싶다고 가정해 봅니다.

Korean 생성자에서 Person을 직접 호출하면 될까?

javascript
function Korean(name, city) {
    Person(name);

    // Korean의 인스턴스 멤버 정의
    this.city = city;
}

이렇게 하고 Korean의 인스턴스를 생성하고 name에 접근해 봅니다.

javascript
function Person(name) {
    // Person의 인스턴스 멤버 정의
    this.name = name;
}

function Korean(name, city) {
    Person(name);

    // Korean의 인스턴스 멤버 정의
    this.city = city;
}
var mySon = new Korean('재희', '서울');
console.log(mySon.name); // undefined 를 반환

보다시피 name이 Korean의 인스턴스에 정의되지 않았다고 undefined가 출력됩니다.

앞에서처럼 직접 Korean 생성자에서 Person을 호출할 때 Person 멤버를 소유하는 객체를 지정하지 않았습니다.

따라서 Person을 루트 객체의 멤버로 간주하고 Person 내부에서 사용되는 this는 루트 객체를 가리키게 됩니다.

브라우저에서는 Window 객체를 가리킨다.

따라서 위와 같은 경우는 전역 변수 스코프의 루트 객체에 name 속성을 추가하는 셈입니다.


이러한 경우 Function의 call 또는 apply를 사용해 Person 내부에서 사용되는 this를 원하는 객체를 바라보게 할 수 있습니다.

다른 말로 하면 Person의 실행 컨텍스트를 Korean의 실행 컨텍스트로 설정할 수 있다는 것입니다.


Function의 call,apply를 사용하면 함수를 다른 함수의 컨텍스트에서 실행할 수 있다.


수정된 Korean 생성자 코드는 다음과 같습니다.

javascript
function Person(name) {
    // Person의 인스턴스 멤버 정의
    this.name = name;
}

function Korean(name, city) {
    // 부모 생성자를 호출한다.
    // 이때 부모 생성자 내의 this에는 call 또는 apply 함수의 첫 번째 인자를 할당한다.
    // 다음 코드는 Korean 타입의 인스턴스 this를 Person 생성자의 this에 할당한다.
    // 결국, 부모 생성자 Person에서는 인자로 전달 받은 Korean 인스턴스에 name 속성을 추가하는 셈이다.
    Person.apply(this, [name]); // 또는 Person.call(this, name);

    // Korean의 인스턴스 멤버 정의
    this.city = city;
}

var mySon = new Korean('재희', '서울');
console.log(mySon.name); // '재희' 를 반환

new Korean을 실행하면 Korean 인스턴스를 생성하고 나서 Korean 생성자를 호출합니다.

이때 생성된 인스턴스를 Korean의 this에 할당합니다. 

Korean 생성자 내부에서 Person.call/apply를 호출하면 방금 생성된 Korean 인스턴스를 Person 생성자의 this로 전달합니다.

결국 Person 생성자 내부의 this.name = name 은 생성된 Korean 인스턴스에 name 속성 멤버를 추가하는 결과를 가져오게 되는 것입니다.



상속 구현 통합

이제 프로토타입 상속과 인스턴스 멤버 상속를  함께 구현하는 경우를 생각해 보도록 하겠습니다.

앞에서 본 프로토타입 상속을 구현하는 코드를 다시 살펴봅니다.

javascript
Korean.prototype = new Person();
Korean.prototype.constructor = Korean;

첫 번째 코드에 의해 Korean의 프로토타입 객체는 Person 인스턴스 객체로 대체됩니다.

그런데 프로토타입 객체에 있는 name은 인스턴스별로 존재하는 것이기에 프로토타입 멤버로는 대부분 필요없을 것입니다.

따라서 다음과 같이 프로토타입 멤버에서 name 속성을 제거하면 상속이 완성됩니다.

javascript
delete Korean.prototype.name;

프로토타입 멤버에서 name 속성을 반드시 제거해야만 하는 것은 아닙니다.

인스턴스 멤버가 프로토타입 멤버로 전활될 때 일반적으로 불필요하기 때문에 제거하는 것이 일반적이지만 그대로 두어도 상관은 없습니다.

이런 작업은 선택적입니다.


인스턴스 멤버 상속과 프로토타입 멤버 상속을 구현하는 코드를 합쳐보도록 합니다.

Person과 Korean에는 프로토타입 멤버인 species, nationality가 포함돼 있습니다.

javascript
// Person 생성자
function Person(name) {
    this.name = name;
}
// Person의 프로토타입 멤버 정의
Person.prototype.species = 'human';

// Korean 생성자
function Korean(name, city) {
    // 인스턴스 멤버 상속
    Person.call(this, name); // 또는 Person.apply(this, [name]);

    // Korean의 인스턴스 멤버
    this.city = city;
}
Korean.prototype.nationality = 'korean';

// 프로토타입 멤버 상속
Korean.prototype = new Person();
Korean.prototype.constructor = Korean;

// 다음 코드는 상속 구현에서 선택적이다
delete Korean.prototype.name;

이제 Korean 타입의 인스턴스를 mySon을 생성하고 모든 멤버를 출력해 보도록 합니다.

javascript
var mySon = new Korean('재희님', 'seoul');
// 멤버 출력
// 속성을 담을 배열 선언
var arr = [];
for(var propertyName in mySon) {
    arr.push(propertyName);
}
// 배열 출력
console.log(arr); // ["name", "city", "constructor", "species"]




멤버 확장

앞에서는 생성자의 prototype 속성을 이용해 프로토타입 멤버를 상속하는 것과 Function의 call/apply 함수를 이용해 인스턴스 멤버를 상속하는 방법에 대해 알아보았습니다.

그러나 자바스크립트에서는 상속을 흉내낼 수 있는 다른 간단한 방법도 있습니다.


상속이란 결국 부모 객체의 멤버를 모두 자식 객체에서 사용할 수 있게 하는 것입니다.

따라서 부모 객체의 모든 멤버를 자식 객체로 복사하는 것도 좀 단순해 보이긴 하지만 자바스크립트에서 간단하게 상속을 구현하는 방법이라고 할 수 있습니다.


부모 객체를 자식 객체로 복사하는 작업에는 for/in 을 이용할 수 있습니다.

다음과 같은 유틸리티 메서드를 Object의 프로토타입 멤버로 만들어두면 편하게 멤버 복사를 할 수 있습니다.

javascript
Object.prototype.extend = function (parent) {
    for(var property in parent) {
        this[property] = parent[property];
    }
};


for/in을 이용하면 사용자가 정의한 모든 프로토타입 멤버와 인스턴스 멤버(속성,메서드)에 접근할 수 있습니다.

위에서 정의한 메서드는 다음과 같이 사용할 수 있습니다.

javascript
Object.prototype.extend = function (parent) {
    for(var property in parent) {
        this[property] = parent[property];
    }
};

function Person(name) {
    this.name = name;
}
Person.prototype.setNewName = function (newName) {
    this.name = newName;
};
// 부모 객체 생성
var parent = new Person('재희야');

// 자식 객체 생성
var child = {};

// 멤버 상속
child.extend(parent)

child.extend(parent)를 실행할 때 extend 메서드 내부에서 사용되는 this는 child 객체를 가리킨다는 것을 알 수 있을 것입니다.

이런 식으로 child 객체의 멤버를 확장하면 extend에서 사용하는 for/in의 특징 때문에 parent의 모든 인스턴스 멤버와 프로토타입 멤버는 child의 인스턴스 멤버로 추가됩니다.

child에 추가된 멤버를 출력해 보면 다음과 같습니다.

javascript
var arr = [];
for (var property in child) {
    if(child.hasOwnProperty(property)) { // 인스턴스 멤버인지 확인
        arr.push(property);
    }
}
console.log(arr); // ["name", "setNewName", "extend"] 반환





Jaehee's WebClub