6. 자바스크립트 객체지향 프로그래밍 #2 상속

2021. 11. 11. 15:39JavaScript/basic

이전글

https://yonghwankim-dev.tistory.com/165

 

6. 자바스크립트 객체지향 프로그래밍 #1 클래스, 생성자, 메서드

이전글 https://yonghwankim-dev.tistory.com/164 5. 실행 컨텍스트와 클로저 #4 클로저(Closure) 이전글 https://yonghwankim-dev.tistory.com/163 5. 실행 컨텍스트와 클로저 #3 스코프 체인 이전글 https://yon..

yonghwankim-dev.tistory.com

 

본 글은 INSIDE JAVASCRIPT 도서의 내용을 복습하기 위해 작성된 글입니다.

 

1. 프로토타입을 이용한 상속

아래의 코드는 더글라스 크락포드가 자바스크립트 객체를 상속하는 방법으로 오래 전에 소개한 코드입니다.

function create_object(obj){
    function F(){}

    F.prototype = obj;

    return new F();

}

위 코드를 그림으로 표현하면 아래 그림과 같습니다.

create_object() 함수는 인자로 들어온 obj 객체를 부모(obj)로 설정하고 obj를 부모로 하는 자식 객체(new F())를 생성하여 반환합니다. create_object() 함수의 수행과정을 분석하면 다음과 같습니다.

  1. 새로운 빈 함수 객체 F 생성(function F() { } )
  2. F.prototype 프로퍼티는 인자로 들어온 obj를 가리킴(F.prototype = obj)
  3. 함수 객체 F를 생성자로 하는 새로운 객체를 생성하여 반환(return new F())

위와 같은 과정을 수행하여 반환받은 자식 객체는 부모 객체(obj)의 프로퍼티에 접근할 수있고, 자신만의 프로퍼티를 생성할 수 있습니다. 위와 같이 프로토타입의 특성을 활용하여 상속을 구현하는 것이 프로토타입 기반의 상속입니다.

 

아래의 코드는 위의 create_object() 함수를 이용하여 상속을 구현한 예제입니다.

const person = {
    name : "zzoon",
    getName : function(){
        return this.name;
    },
    setName : function(name){
        this.name = name;
    }
};

function create_object(obj){
    function F(){

    };

    F.prototype = obj;

    return new F();
}

const student = create_object(person);
student.setName("kim");
console.log(student.getName()); // Expected Output : kim

위 예제를 통하여 person 객체를 상속하여 student 객체를 생성하였습니다. 위 프로토타입 기반 상속의 특징은 다음과 같습니다.

  • 클래스에 해당하는 생성자 함수를 만들지 않음(function Person(arg){ ... } )
  • 그 클래스의 인스턴스를 따로 생성하지도 않음(new Person("kim))
  • 단지 부모 객체에 해당하는 person 객체와 이 객체를 프로토타입 체인으로 참조할 수 있는 자식 객체 student를 만들어서 사용함

위 예제의 관계를 그림으로 표현하면 아래와 같습니다.

 

위 예제를 통하여 부모 객체의 메서드를 그대로 상속받아 사용하는 방법을 살펴보았습니다. 여기에서 자식은 자신의 메서드를 재정의 혹은 추가로 기능을 더 확장시킬 수 있어야합니다.

student.getAge = function() {...}
student.setAge = function(age) {...}

단순히 위와 같은 방법으로 기능을 확장시킬 수 있습니다. 하지만 위와 같이 구현하면 코드의 가독성이 떨어질 것입니다. 그래서 자바스크립에서는 범용적으로 extend()라는 이름의 함수로 객체에 자신이 원하는 객체 혹은 함수를 추가시킵니다. 아래의 코드는 jQuery의 extend() 함수의 내용입니다.

jQuery.extend = jQuery.fn.extend = function(obj, prop){
    if(!prop){
    	prop = obj;
        obj = this;
    }
    for( var i in prop){
    	obj[i] = prop[i];
    }
    return obj;
}

 

jQuery.extend = jQuery.fn.extend = function(obj, prop){

위 코드의 일부를 분석하면 jQuery.fn은 jQuery.prototype입니다. 따라서 위 코드가 의미하는 바는 jQuery 함수 객체와 jQuery 함수 객체의 인스턴스 모두 extend 함수를 사용할 수 있다는 의미입니다. 즉, 아래 코드와 같이 호출할 수 있습니다.

// using 1
jQuery.extend();

// using 2
var elem = new JQuery(..);
elem.extend();

 

if(!prop){
	prop = obj;
    obj = this;
}

위 코드의 의미는 extend 함수의 인자가 하나만 들어오는 경우에는 현재 객체(this)에 인자로 들어오는 객체의 프로퍼티를 복사함을 의미합니다. 만약 extend 함수의 인자가 두개가 들어오는 경우에는 첫 번째 객체에 두 번째 객체의 프로퍼티를 복사하겠다는 의미입니다.

 

for(var i in prop){
	obj[i] = prop[i];
}

위 코드의 의미는 prop의 프로퍼티를 obj로 복사합니다.

 

하지만 위 extend() 함수의 코드에는 약점이 존재합니다. 그것은 'obj[i] = prop[i];' 명령문이 얕은 복사(shallow copy)를 의미합니다. 즉, 문자 혹은 숫자 리티럴 등이 아닌 객체(배열, 함수 객체 포함)인 경우 해당 객체를 복사하지 않고, 참조합니다. 이는 두 번째 객체의 프로퍼티가 변경되면 첫 번짹 객체의 프로퍼티도 같이 변경됨을 의미합니다.

 

extend() 함수를 적용하여 자식 객체의 기능 추가

const person = {
    name : "zzoon",
    getName : function(){
        return this.name;
    },
    setName : function(name){
        this.name = name;
    }
};

function create_object(obj){
    function F(){

    };

    F.prototype = obj;

    return new F();
}

function extend(obj, prop){
    if(!prop){
        prop = obj;
        obj = this;
    }
    for(var i in prop){
        obj[i] = prop[i];
    }
}

const student = create_object(person);
const added = {
    setAge : function(age){
        this.age = age;
    },
    getAge : function(){
        return this.age;
    }
};

// added의 프로퍼티를 student에 추가
extend(student, added);

student.setAge(25);
console.log(student.getAge());  // Expected Output : 25

얕은 복사를 사용하는 extend() 함수를 사용하여 student 객체를 확장시킬 수 있었습니다. extend() 함수는 사용자에게 유연하게 기능 확장을 할 수 있게 하는 주요 함수일뿐 아니라, 상속에서도 자식 클래스를 확장할 때 유용하게 사용됩니다.

2. 클래스 기반의 상속

클래스 기반의 상속이라고는 하나 프로토타입을 이용한 상속에서 소개한 내용과 거의 같습니다. 앞 절처럼 함수의 프로토타입을 적절히 엮어서 상속을 구현해냅니다. 다만 앞 절에서는 객체 리터럴로 생성된 객체의 상속을 소개했지만, 여기서는 클래스의 역할을 하는 함수로 상속을 구현합니다.

 

function Person(name){
    this.name = name;
}

Person.prototype.getName = function(){
    return this.name;
}

Person.prototype.setName = function(name){
    this.name = name;
}

function Student(name){

}

const kim = new Person("kim");
Student.prototype = kim;

const me = new Student("zzoon");
me.setName("zzoon");
console.log(me.getName());  // Expected Output : zzoon

위 예제의 상속 관계를 그림으로 표현하면 아래 그림과 같습니다.

  • Student 함수 객체를 생성하여, Person 함수 객체의 인스턴스를 가리키도록 함
  • me 객체의 [[Prototype]] 링크는 kim 객체를 가리킴
  • kim 객체의 [[Prototype]] 링크는 Person.prototype 프로토타입 객체를 가리킴
  • me 객체는 Person.prototype 객체의 프로퍼티에 접근이 가능하고 getName(), setName() 함수를 호출 가능함

하지만 위 예제는 문제점이 존재합니다. 첫번째는 me 인스턴스를 생성할 때 부모 클래스인 Person의 생성자를 호출하지 않습니다.

const me = new Student("zzoon");

new Student("zzoon"); 명령문은 Student 함수 객체가 비어있기 때문에 수행되지 않습니다. 결국 생성된 me 객체는 setName() 메서드를 호출되고 나서야 me 객체에 name 프로퍼티가 생성됩니다. 이렇게 부모의 생성자가 호출되지 않으면, 인스턴스의 초기화가 제대로 이루어지지 않아 문제가 발생합니다. 위 문제를 해결하려면 Student 함수에 아래의 코드를 추가하여 부모 클래스의 생성자를 호출하여야 합니다.

function Student(name){
    Person.apply(this, arguments);
}

Student 함수 안에서 새롭게 생성된 객체(this, empty 객체)를 apply 함수의 첫번째 인자로 넘겨 Person 함수를 실행시킵니다. 위와 같은 방식으로 자식 클래스의 인스턴스에 대해서도 부모 클래스의 생성자를 실행시킬 수 있습니다.

 

두번재 문제는 자식 클래스의 객체가 부모 클래스의 객체를 프로토타입 체인으로 직접 접근하여 종속적이라는 문제점을 가지고 있습니다. 즉, 자식 클래스의 prototype에 메소드를 추가할 때 문제가 됩니다.

 

 

두 클래스의 프로토타입 사이에 중개자를 둔 문제 해결

function Person(name){
    this.name = name;
}

Function.prototype.method = function(name,func){
    this.prototype[name] = func;
}

Person.method("getName",function(){
    return this.name;
});

Person.method("setName",function(name){
    this.name = name;
});

function Student(arg){

}

function F(){

}

F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;

Student.super = Person.prototype;

const me = new Student();
me.setName("zzoon");
console.log(me.getName());

위 예제를 통하여 빈 함수 F()를 생성하고, 이 F()의 인스턴스를 Person.prototype과 Student 사이에 두어 중개자 역할을 수행하도록 합니다. 그리고 이 F()의 인스턴스를 Student.prototype이 참조하게 하여 Student 클래스의 기능 확장을 수행하도록 합니다. 위와 같은 관계를 그림으로 표현하면 아래와 같습니다.

 

즉시 실행함수와 클로저를 활용한 상속 관계 최적화

const inherit = function(Parent, Child){
    const F = function(){

    };

    return function(Parent, Child){
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
        Child.super = Person.prototype;
    };
}();

const kim = new Student();
inherit(Person,kim);    // kim을 Person의 자식으로 설정
kim.setName("kim");
console.log(kim.getName());    // Expected Output : kim

위 코드에서 클로저(반환되는 함수)는 F() 함수를 지속적으로 참조하므로, F()는 가비지 컬렉에의 대상이 되지 않고 계속 남을 수 있게 됩니다. 이를 활용해 함수 F()의 생성은 단 한번만 이루어지고 inherit 함수가 계속해서 호출되어도 함수 F()의 생성을 새로할 필요가 없게 됩니다.

 

References

source code : https://github.com/yonghwankim-dev/javascript\_study
송형주, 고현준 저, 『인사이드 자바스크립트』, 한빛미디어(2014)