IT_Programming/JavaScript

자바스크립트 완벽사이드 - 9.6 상속 없이 확장하기

JJun ™ 2011. 4. 29. 13:35

 


9.6 상속 없이 확장하기

 

-      자바스크립트 함수들은 데이터 값이므로 함수를 한 클래스에서 다른 클래스로 복사할 수 있다.

 

// Borrow methods from one class for use by another.

// The arguments should be the constructor functions for the classes

// Methods of built-in types such as Object, Array, Date and RegExp are

// not enumerable and cannot be borrowed with this method.

function borrowMethods(borrowFrom, addTo) {

    var from = borrowFrom.prototype;  // prototype object to borrow from

    var to = addTo.prototype;         // prototype object to extend

 

    for(m in from) {  // Loop through all properties of the prototye

        if (typeof from[m] != "function") continue; // ignore nonfunctions

        to[m] = from[m];  // borrow the method

    }

}

 

 

-      많은 메서드들은 자신을 정의한 클래스에 튼튼하게 연결되어 있으므로 메서드를
다른 클래스에서 사용하려는 것은 말이 되지 않는 것 같지만, 일반적으로 메서드를
클래스나 특정한 프로퍼티를 정의하는 클래스에서 사용하기 적합하게 작성할 수 있다.

 

// This class isn't good for much on its own. But it does define a

// generic toString() method that may be of interest to other classes.

function GenericToString() {}

GenericToString.prototype.toString = function() {

    var props = [];

    for(var name in this) {

        if (!this.hasOwnProperty(name)) continue;

        var value = this[name];

        var s = name + ":"

        switch(typeof value) {

        case 'function':

            s += "function";

            break;

        case 'object':

            if (value instanceof Array) s += "array"

            else s += value.toString();

            break;

        default:

            s += String(value);

            break;

        }

        props.push(s);

    }

    return "{" + props.join(", ") + "}";

}

 

// This mixin class defines an equals() method that can compare

// simple objects for equality.

function GenericEquals() {}

GenericEquals.prototype.equals = function(that) {

    if (this == that) return true;

   

    // this and that are equal only if this has all the properties of

    // that and doesn't have any additional properties

    // Note that we don't do deep comparison.  Property values

    // must be === to each other.  So properties that refer to objects

    // must refer to the same object, not objects that are equals()

    var propsInThat = 0;

    for(var name in that) {

        propsInThat++;

        if (this[name] !== that[name]) return false;

    }

 

    // Now make sure that this object doesn't have additional props

    var propsInThis = 0;

    for(name in this) propsInThis++;

   

    // If this has additional properties then they are not equal

    if (propsInThis != propsInThat) return false;

 

    // The two objects appear to be equal.

    return true;

}

 

위의 예제는 아무 작업은 하지 않지만 다른 클래스가 빌려갈 수 있는 유용한 메서드들을 정의하는 클래스를 두 개 보여준다.

 

이와 같이 빌려주는 작업을 수행할 목적으로 만들어진 클래스를 믹스인 클래스 혹은 믹스인 이라고 부른다.

 

 

 

-      아래는 믹스인 클래스가 정의한 toString() 메서드와 equals() 메서드를 빌리는 Rectangle 클래스이다.

 

function Rectangle (x, y, w, h) {

   this.x = x;

this.y = y;

this.w = w;

this.h = h;

}

 

Rectangle.prototype.area = function() {

   return this.width * this.height;

}

 

borrowMethods(GenericEqual, Rectangle);

borrowMethods(GenericToString, Rectangle);

 

 

 

 

-      원한다면 생성자 함수도 빌려올 수 있다.
아래의 ColoreRectangle 클래스는 Rectangle 클래스에서 작성된 사각형과 관련된 기능들을 상속 받고, Colored라는 믹스인에서 생성자와 메서드를 빌려온다.

 

// 이 믹스인에는 생성자에 의존하는 메서드가 들어있다.
//
생성자와 이 메서드를 모두 빌려와야 한다.

function Colored(c) {

   this.color = c;

}

 

Colored.prototype.getColor = function() {

   Return this.color;

}

 

// 새 클래스를 위한 생성자 정의

function ColoredRectangle (x, y, w, h, c) {

   this.superclass(x, y, w, h);  // 상위 클래스 생성자 호출

   Colored.call(this, c);  // 그리고 Colored 생성자를 빌려온다.

}

 

// Rectangle 클래스에서 메서드를 상속 받기 위해 프로토 타입 객체를 설정

ColoredRectangle.prototype = new Rectangle();

ColoredRectangle.prototype.constructor = ColoredRectangle;

ColoredRectangle.prototype.superclass = Rectangle;

 

// 그리고 새 클래스를 위해 Colored의 메서드를 빌려온다.

borrowMethod(Colored, ColoredRectangle);

 

 

 

-      아래의 예제는 객체가 다른 클래스의 메서드를 빌려왔는지를 검사하는 예제다. (엄격함)

 

// Return true if each of the method properties in c.prototype have been

// borrowed by o. If o is a function rather than an object, we

// test the prototype of o rather than o itself.

// Note that this function requires methods to be copied, not

// reimplemented.  If a class borrows a method and then overrides it,

// this method will return false.

function borrows(o, c) {

    // If we are an instance of something then of course we have its methods

    if (o instanceof c) return true;

 

    // It is impossible to test whether the methods of a built-in type have

    // been borrowed, since the methods of built-in types are not enumerable.

    // We return undefined in this case as a kind of "I don't know" answer

    // instead of throwing an exception. Undefined behaves much like false,

    // but can be distinguished from false if the caller needs to.

    if (c == Array || c == Boolean || c == Date || c == Error ||

        c == Function || c == Number || c == RegExp || c == String)

        return undefined;

 

    if (typeof o == "function") o = o.prototype;

    var proto = c.prototype;

    for(var p in proto) {

        // Ignore properties that are not functions

        if (typeof proto[p] != "function") continue;

        if (o[p] != proto[p]) return false;

    }

    return true;

}

 

 

 

-      아래는 배열이 같은지에 대한 비교를 하는 함수 예제이다.

 

function isArrayLike(x) {

    if (x instanceof Array) return true; // Real arrays are array-like

    if (!("length" in x)) return false;  // Arrays must have a length property

    if (typeof x.length != "number") return false;  // Length must be a number

    if (x.length < 0) return false;                 // and nonnegative

    if (x.length > 0) {

        // If the array is nonempty, it must at a minimum

        // have a property defined whose name is the number length-1

        if (!((x.length-1) in x)) return false;

    }

    return true;

}

 

 

 

-      아래는 앞서 설명한 기능들을 클래스로 만든 예제이다.

 

/**

 * defineClass() -- a utility function for defining JavaScript classes.

 *

 * This function expects a single object as its only argument.  It defines

 * a new JavaScript class based on the data in that object and returns the

 * constructor function of the new class.  This function handles the repetitive

 * tasks of defining classes: setting up the prototype object for correct

 * inheritance, copying methods from other types, and so on.

 *

 * The object passed as an argument should have some or all of the

 * following properties:

 *

 *   name: the name of the class being defined.

 *          If specified, this value will be stored in the classname

 *          property of the prototype object.

 *

 *   extend: The constructor of the class to be extended.  If omitted,

 *           the Object() constructor will be used.  This value will

 *           be stored in the superclass property of the prototype object.

 *

 *   construct: The constructor function for the class. If omitted, a new

 *             empty function will be used.  This value becomes the return

 *             value of the function, and is also stored in the constructor

 *             property of the prototype object.

 *

 *   methods: An object that specifies the instance methods (and other shared

 *            properties) for the class.  The properties of this object are

 *            copied into the prototype object of the class.  If omitted,

 *            an empty object is used instead.  Properties named

 *            "classname", "superclass", and "constructor" are reserved

 *            and should not be used in this object.

 *

 *   statics: An object that specifies the static methods (and other static

 *            properties) for the class.  The properties of this object become

 *            properties of the constructor function.  If omitted, an empty

 *            object is used instead.

 *

 *   borrows: A constructor function or array of constructor functions.

 *            The instance methods of each of the specified classes are copied

 *            into the prototype object of this new class so that the

 *            new class borrows the methods of each specified class.

 *            Constructors are processed in the order they are specified,

 *            so the methods of a class listed at the end of the array may

 *            overwrite the methods of those specified earlier. Note that

 *            borrowed methods are stored in the prototype object before

 *            the properties of the methods object above.  Therefore,

 *            methods specified in the methods object can overwrite borrowed

 *            methods. If this property is not specified, no methods are

 *            borrowed.

 *

 *  provides: A constructor function or array of constructor functions.

 *            After the prototype object is fully initialized, this function

 *            verifies that the prototype includes methods whose names and

 *            number of arguments match the instance methods defined by each

 *            of these classes.  No methods are copied; this is simply an

 *            assertion that this class "provides" the functionality of the

 *            specified classes.  If the assertion fails, this method will

 *            throw an exception.  If no exception is thrown, any

 *            instance of the new class can also be considered (using "duck

 *            typing") to be an instance of these other types.  If this

 *            property is not specified, no such verification is performed.

 **/

function defineClass(data) {

    // Extract the fields we'll use from the argument object.

    // Set up default values.

    var classname = data.name;

    var superclass = data.extend || Object;

    var constructor = data.construct || function() {};

    var methods = data.methods || {};

    var statics = data.statics || {};

    var borrows;

    var provides;

 

    // Borrows may be a single constructor or an array of them.

    if (!data.borrows) borrows = [];

    else if (data.borrows instanceof Array) borrows = data.borrows;

    else borrows = [ data.borrows ];

 

    // Ditto for the provides property.

    if (!data.provides) provides = [];

    else if (data.provides instanceof Array) provides = data.provides;

    else provides = [ data.provides ];

 

    // Create the object that will become the prototype for our class.

    var proto = new superclass();

 

    // Delete any noninherited properties of this new prototype object.

    for(var p in proto)

        if (proto.hasOwnProperty(p)) delete proto[p];

 

    // Borrow methods from "mixin" classes by copying to our prototype.

    for(var i = 0; i < borrows.length; i++) {

        var c = data.borrows[i];

        borrows[i] = c;

        // Copy method properties from prototype of c to our prototype

        for(var p in c.prototype) {

            if (typeof c.prototype[p] != "function") continue;

            proto[p] = c.prototype[p];

        }

    }

 

    // Copy instance methods to the prototype object

    // This may overwrite methods of the mixin classes

    for(var p in methods) proto[p] = methods[p];

 

    // Set up the reserved "constructor", "superclass", and "classname"

    // properties of the prototype.

    proto.constructor = constructor;

    proto.superclass = superclass;

    // classname is set only if a name was actually specified.

    if (classname) proto.classname = classname;

 

    // Verify that our prototype provides all of the methods it is supposed to.

    for(var i = 0; i < provides.length; i++) {  // for each class

        var c = provides[i];

        for(var p in c.prototype) {   // for each property

            if (typeof c.prototype[p] != "function") continue;  // methods only

            if (p == "constructor" || p == "superclass") continue;

            // Check that we have a method with the same name and that

            // it has the same number of declared arguments.  If so, move on

            if (p in proto &&

                typeof proto[p] == "function" &&

                proto[p].length == c.prototype[p].length) continue;

            // Otherwise, throw an exception

            throw new Error("Class " + classname + " does not provide method "+

                            c.classname + "." + p);

        }

    }

 

    // Associate the prototype object with the constructor function

    constructor.prototype = proto;

 

    // Copy static properties to the constructor

    for(var p in statics) constructor[p] = data.statics[p];

 

    // Finally, return the constructor function

    return constructor;

}

 

 


 

[사용법]

 

// A Comparable class with an abstract method

// so that we can define classes that "provide" Comparable.

var Comparable = defineClass({

    name: "Comparable",

    methods: { compareTo: function(that) { throw "abstract"; } }

});

 

// A mixin class with a usefully generic equals() method for borrowing

var GenericEquals = defineClass({

    name: "GenericEquals",

    methods: {

        equals: function(that) {

            if (this == that) return true;

            var propsInThat = 0;

            for(var name in that) {

                propsInThat++;

                if (this[name] !== that[name]) return false;

            }

 

            // Now make sure that this object doesn't have additional props

            var propsInThis = 0;

            for(name in this) propsInThis++;

 

            // If this has additional properties then they are not equal

            if (propsInThis != propsInThat) return false;

 

            // The two objects appear to be equal.

            return true;

        }

    }

});

 

 

// A very simple Rectangle class that provides Comparable

var Rectangle = defineClass({

    name: "Rectangle",

    construct: function(w,h) { this.width = w; this.height = h; },

    methods: {

        area: function() { return this.width * this.height; },

        compareTo: function(that) { return this.area() - that.area(); }

    },

    provides: Comparable

});

 

 

// A subclass of Rectangle that chains to its superclass constructor,

// inherits methods from its superclass, defines an instance method and

// a static method of its own, and borrows an equals() method.

var PositionedRectangle = defineClass({

    name: "PositionedRectangle",

    extend: Rectangle,

    construct: function(x,y,w,h) {

        this.superclass(w,h);  // chain to superclass

        this.x = x;

        this.y = y;

    },

    methods: {

        isInside: function(x,y) {

            return x > this.x && x < this.x+this.width &&

                y > this.y && y < this.y+this.height;

        }

    },

    statics: {

        comparator: function(a,b) { return a.compareTo(b); }

    },

    borrows: [GenericEquals]

});