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] });
|
'IT_Programming > JavaScript' 카테고리의 다른 글
[Tip] 자바스크립트 onload 시점 (0) | 2013.02.06 |
---|---|
[펌_번역] Private Members in JavaScript -Douglas Crockford (0) | 2011.05.31 |
자바스크립트 완벽가이드 - 9.5 슈퍼 클래스와 서브 클래스 (0) | 2011.04.29 |
[JQuery] Ajax IE 캐싱 문제 (0) | 2011.04.03 |
IE 에서 location.href 를 쓰는 경우 referer 값을 제대로 받아오는 방법 (0) | 2011.04.03 |