[번역] Correct OOP for Javascript -Shelby H. Moore III. linguistics

Correct OOP for Javascript -Shelby H. Moore III.


Incorrect

자바스크립트의 OOP와 상속 구현에 관한 수많은 아티클들 내용중에는 상당수 잘못 알려진 근본적인 결함이 있다.

예를 들어, Gavin Kistner 의 글(http://phrogz.net/JS/Classes/OOPinJS2.html)에 등장하는 첫번째 소스코드를 분석해 보자. 이 글은 2006년 1월에 모질라의 개발자 문서의 대문에 링크되어 중요하게 다루어졌음을 인지하기 바란다.

결함을 확인하기 위해, 다음 코드를 Gavin 의 첫번째 예의 코드에 추가해 보자.

var myPet2 = new Cat('Felix2');
alert('myPet2 is' + myPet2);        // 결과 => 'myPet is [Cat "Felix2"]'
myPet2.haveABaby();                // Mammal 로부터 상속받은 메소드를 호출함.
alert(myPet2.offspring.length);    // 여기서 고양이가 2마리 새끼를 갖고 있음을 보게 됨.
alert(myPet2.offspring[1]);        // 결과 => '[Mammal "Baby Felix2"]'

myPet2 는 offspring 1개를 갖고 있어야 하지만, 2개를 갖는 것으로 결과가 나왔다. 이유는 다음의 코드 때문이다.

Cat.prototype = new Mammal();

이 코드는 Cat 의 프로토타입 체인에 단 하나의 Mammal 인스턴스를 지정하고 있으며, 이로인해 Cat의 모든 인스턴스들은 Mammal 인스턴스 프로퍼티이며 결과적으로 단 하나밖에 존재하지 않는 offspring 프로퍼티를 수정하게 된다. parent 클래스의 인스턴스 멤버가 프로토타입 멤버가 되면, 모든 인스턴스에 공유되게 된다.


Correct

이 결함은 "마스킹 효과;Masking effect" 라는 것을 사용하여 피할 수 있다. 이 효과는 자바스크립트가 객체의 멤버를 찾는 순서에 영향을 준다. object.identifier 를 찾을 때, ?? object.identifier 가 자바스크립트의 data type 을 가리키는 지점에서 ??, 자바스크립트는 다음과 같은 일을 수행한다.
(The fix to this flaw employs the "masking effect" of the order in which Javascript searches for elements (properties and methods) of an object. When resolving object.identifier, where object.identifier is a reference to a Javascript data type (e.g. Function, Object, Number, or String), then Javascript does the equivalent of:)

function resolve( identifier, object )
{
    for ( var element in object )
    {
        if (element == identifier)
        {
            return object.element;
        }
    }
    if ( object.constructor.prototype != null )
    {
        return resolve( identifier, object.constructor.prototype )
    }
    return "undefined";
}

해서, 만약 identifier 가 child 클래스에 존재한다면, 그것이 프로토타입의 다른 모든 중복된 identifier 를 가리게;mask 된다. 뻔히 보이는 방법이긴 해도 다음과 같이 미봉책으로 offspring 프로퍼티를 Cat 에 추가할 수 있다.

function Cat(name) {
    this.name = name;
    this.offspring = [];
}

그러나, 이래서는 상속의 의미가 무색해진다. child 클래스인 Cat 을 구현하기 위해, parent 클래스인 Mammal 의 내부을 알아야 한다면, data 캡슐화와 나아가서 OOP 를 구현한다고 할 수 없다. 또한, Cat 생성자의 name 매개변수는 Mammal  생성자에 전달되지 않음을 인지해야 한다. 이는 Mammal 생성자의 data 캡슐화를 깨뜨리는데, ?? 생성자가 생성시에 name 프로퍼티에의 값 지정외에 아무것도 하지 않다면 말이다 ??.
(But this defeats the purpose of inheritance. If we need to know the internal datastructure of Mammal (the parent class) in order to implement Cat (the child class), then we don't have data encapsulation and thus we don't have OOP (Object Oriented Programming). Also note that name argument of the constructor of Cat is not passed to Mammal's constructor. This violates the data encapsulation of constructor of Mammal, as it assumes the constructor does nothing more than assignment to the name property on construction.)

data 캡슐화를 유지하는 보편화된 방법을 제시한 이들이 있다(fm.dept-z.com/index.asp?get=/Resources/OOP_with_ECMAScript/Inheritance). Function.call 과 Function.apply 에 대한 모질라 자바스크립트 문서에서 또한 언급되기도 했다.

function Cat(name)
{
    Mammal.call( this, name );
}

위의 코드는 Mammal 의 생성자를 호출하는데, ('this' 가 의미하는 바 대로) 새로운 Cat object 의 scope 에서 실행한다. 이는 Cat object 내에서 Mammal 의 모든 요소들을 생성하게 된다.


Details

child 생성자 object 의 scope 에서 parent 의 생성자를 실행하는 것이 덧붙여 우리는, child 의 prototype 계층에 parent 의 prototype 을 포함시켜야한다.

Cat.prototype = new Mammal();

Mammal 생성자는 name 매개변수를 필요로 하기 때문에, error 가 발생한다. 더미 변수를 넘기기로 하고 parent 생성자에서는 "undefined" type 변수를 받지 않거나, parent 생성자가 undefined 변수까지 처리하거나 둘중에 하나로 해결할 수 밖에 없다. 다음과 같이 생성자를 수정할 수 있다.

function Mammal(name) {
    if ( typeof(name) == "undefined" ) {
        name = "";
    }
    this.name = name;
    this.offspring = [];
}

??
일단 기본적으로, child 생성자의 프로토타입을 빈 Object 로 설정하고 child 의 생성자를 갖게 한다.
(By default, Javascript sets the prototype of the child constructor to an empty Object, but with the child's constructor:)

Cat.prototype = new Object();
Cat.prototype.constructor = Cat;

child 생성자로 object 를 생성할 때, 다음과 같이 한다.
(When an object of the child's constructor is created, Javascript does the equivalent of:)

var object = new Cat();
object.constructor = Cat.prototype.constructor;

prototype 에 object 를 지정할 때는 다음과 같다.
(Yet when assigning an object to a prototype, Javascript does the equivalent of:)

var object = new Mammal();
Cat.prototype = object;
Cat.prototype = constructor = object.constructor;

그래서, child 생성자의 prototype.constructor 를 child 생성자로 설정하는 것은 중요하다 ??.
(Thus, it is important to insure that the child constructor's prototype.constructor is set to the child constructor:)

Cat.prototype = new Mammal();
Cat.prototype.constructor = Cat;


Improved

child 생성자의 object scope 에서 parent 의 생성자를 실행하는 일을 다음과 같이 캡슐화하여 Object.prototype 에 추가하면 편리하게 메소드 호출로 이용할 수 있다.

Object.prototype.Inherits = function(parent)
{
    // parent 의 생성자를 이 object 에 적용함.
    if ( arguments.length > 1 )
    {
        // Note: 'arguments' 는 배열이 아니라 object 이다.
        parent.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    }
    else
    {
        parent.call( this );
    }
}

Cat.prototype = new Mammal();
Cat.prototype.constructor = Cat;
function Cat(name)
{
    this.Inherits( Mammal, name );
}

그리고, prototype 상속도 다음과 같이 Function.prototype 에 추가하여 메소드로 이용할 수 있다.

Function.prototype.Inherits = function( parent )
{
    this.prototype = new parent();
    this.prototype.constructor = this;
}

Cat.Inherits( Mammal );
function Cat( name )
{
    this.Inherits( Mammal, name );
}


Summary

다음 메소드들을 한번만 선언해 주면,

Obejct.prototype.Inherits = function( parent )
{
    if ( arguments.length > 1 )
    {
        parent.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    }
    else
    {
        parent.call( this );
    }
}

Function.prototype.Inherits = function( parent )
{
    this.prototype = new parent();
    this.prototype.constructor = this;
}

다음과 같이 상속 구현이 쉬워진다.

Cat.Inherits( Mammal );
function Cat( name )
{
    this.Inherits( Mammal, this );
}

ColoredCat.Inherits( Cat );
function ColoredCat( name, color )
{
    this.Inherits( Cat, name );
}

Lion.Inherits( ColoredCat );
function Lion( name )
{
    this.Inherits( ColoredCat, name, "gold" );
}

물론 생성자 함수 내에서 입력 매개변수의 undefined 타입검사를 하는 등 각각 기본적인 생성 루틴을 구현해야 하는 것을 잊어서는 안된다.

Copyright (c) 2006, Shelby H. Moore III.