티스토리 뷰

728x90
SMALL

클래스와 프로토타입

자스에서 클래스는 프로토타입 객체로부터 프로퍼티를 상속받은 객체의 집합이다. 따라서 프로토타입 객체는 클래스의 핵심 기능이라고 할 수 있다. 만약 프로토타입 객체를 정의하고 이 프로토타입을 상속하는 객체를 생성하기 위해 inherit 함수를 사용한다면, 자스에서는 클래스를 정의한 것이다.

//range 객체를 반환하는 팩터리 함수
function range(from, to) {
    var r = inherit(range.methods); 
    r.from = from;
    r.to = to;
    return r;
}

range.methods = {
    includes: function(x) { return this.from <= x && x <= this.to; },
    foreach: function(f) {
        for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};

var r = range(1,3);      // Create a range object
r.includes(2);           // => true: 2 is in the range
r.foreach(console.log);  // Prints 1 2 3
console.log(r);          // Prints (1...3)

this는 어떤 객체를 통해 해당 메서드가 호출되었는지를 나타낸다. 이렇게 this를 사용하는 것은 모든 클래스 메서드의 기본 특징이다.

클래스와 생성자

위의 방법처럼 클래스를 정의하는 것은 생성자를 정의하지 않았기 때문에 관용적인 방법은 아니다. 생성자는 새로 생성된 객체를 초기화하는 용도로 사용되는 함수다. 생성자는 new 키워드를 사용하여 호출된다. 생성자를 호출하면 자동으로 새로운 객체가 생성되고, 생성자 함수 내부에서 새로 생성된 객체를 사용하기 때문에, 생성자 함수는 새 객체의 상태를 초기화하는 데만 신경 쓰면 된다. 생성자 호출의 핵심적인 특징은 생성자의 prototype 프로퍼티가 새 객체의 prototype으로 사용된다는 것이다. 이는 한 생성자를 통해 생성된 모든 객체는 같은 객체를 상속하고, 따라서 같은 클래스 멤버를 가지고 있음을 뜻한다.

function Range(from, to) {
    // Store the start and end points (state) of this new range object.
    // These are noninherited properties that are unique to this object.
    this.from = from;
    this.to = to;
}

Range.prototype = {
    includes: function(x) { return this.from <= x && x <= this.to; },
    foreach: function(f) {
        for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};

var r = new Range(1,3);   // Create a range object
r.includes(2);            // => true: 2 is in the range
r.foreach(console.log);   // Prints 1 2 3
console.log(r);           // Prints (1...3)

클래스와 생성자 함수의 이름은 대문자로 시작한다. r이 Range 객체인지를 알고 싶다면, r instanceof Range

instanceof 연산자는 실제로 r이 Range 생성자에 의해 초기화되었는지를 검사하지는 않고 r이 Range.prototype을 상속하는지 여부를 검사한다.

constructor 프로퍼티

위에서 클래스 메서드를 정의한 객체를 Range.prototype으로 설정했다. 이러한 클래스 메서드를 하나의 객체 리터럴에서 정의하는 것이 편리하긴 하지만, 실제로 객체를 생성하는 데 어떤 객체가 꼭 prototype에 설정되어야 할 필요는 없다. 모든 자스 함수는 생성자로 사용될 수 있는데 함수가 생성자로 호출되려면 prototype 프로퍼티가 있어야 한다. 따라서 모든 함수에는 자동으로 prototype 프로퍼티가 설정된다.(bind() 메서드가 반환하는 함수 제외) 이 프로퍼티는 constructor 프로퍼티 하나만 가진 객체다. constructor 프로퍼티의 값은 해당 함수 객체를 가리킨다.

위 코드처럼 prototype을 덮어쓰면 constructor 프로퍼티가 사라진다. 이 문제는 프로토타입 객체에 프로퍼티를 추가하면 된다.

Range.prototype={ constructor:Range, ~~}

다른 기법으로는 프로토타입 객체에 하나씩 메서드를 추가해 나가는 것이다.

Range.prototype.includes=function(x)~~

자바 스타일 클래스

자바 네 가지 클래스 멤버 유형

인스턴스 필드

인스턴스마다 있는 프로퍼티나 변수로, 개별 객체의 상태를 저장한다.

인스턴스 메서드

클래스의 모든 인스턴스가 공유하는 메서드로, 개별 인스턴스에 의해 호출된다.

클래스 필드

인스턴스가 아니라 클래스와 관련된 프로퍼티나 변수다

클래스 메서드

인스턴스가 아니라 클래스와 관련된 메서드다.

 

자스와 자바가 다른 점 한가지는 함수가 값이라는 점이고, 따라서 메서드와 필드를 크게 구분하지 않는다. 만약 프로퍼티 값이 함수라면 그 프로퍼티는 메서드이고, 함수가 아니라면 보통의 프로퍼티나 필드일 뿐이다.

자스에서 클래스 정의에는 세 가지 객체가 관련된다. 그리고 이 객체의 프로퍼티는 일종의 클래스 멤버라고 할 수 있다.

생성자 객체

생성자 함수(객체)는 클래스 이름을 정의한다. 이 생성자 객체에 추가한 프로퍼티는 클래스 필드와 클래스 메서드다.

프로토타입 객체

이 객체의 프로퍼티는 클래스의 모든 인스턴스에 상속된다. 그리고 그 값이 함수인 프로퍼티는 인스턴스 메서드로 작동한다.

인스턴스 객체

각 인스턴스는 자기 자신에 대해 권한을 가진 객체이고, 인스턴스에 직접 정의된 프로퍼티는 다른 인스턴스에 공유되지 않는다. 함수가 아닌 프로퍼티는 클래스의 인스턴스 필드로 작동한다.

 

자스에서 클래스를 정의하는 과정은 세 단계의 알고리즘으로 줄일 수 있다. 먼저, 새 객체의 인스턴스 프로퍼티를 설정하는 생성자 함수를 작성한다. 두 번째, 생성자의 프로토타입 객체에 인스턴스 메서드를 정의한다. 세 번째, 생성자 자체에 클래스 필드와 클래스 프로퍼티를 정의한다.

function Complex(real, imaginary) {
    if (isNaN(real) || isNaN(imaginary)) // Ensure that both args are numbers.
        throw new TypeError();           // Throw an error if they are not.
    this.r = real;                       // The real part of the complex number.
    this.i = imaginary;                  // The imaginary part of the number.
}

Complex.prototype.add = function(that) {
    return new Complex(this.r + that.r, this.i + that.i);
};

Complex.prototype.mul = function(that) {
    return new Complex(this.r * that.r - this.i * that.i,
                       this.r * that.i + this.i * that.r);
};

Complex.prototype.mag = function() {
    return Math.sqrt(this.r*this.r + this.i*this.i);
};

Complex.prototype.neg = function() { return new Complex(-this.r, -this.i); };

Complex.prototype.toString = function() {
    return "{" + this.r + "," + this.i + "}";
};

Complex.prototype.equals = function(that) {
    return that != null &&                      // must be defined and non-null
        that.constructor === Complex &&         // and an instance of Complex 
        this.r === that.r && this.i === that.i; // and have the same values.
};

Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,0);
Complex.I = new Complex(0,1);

Complex.parse = function(s) {
    try {          // Assume that the parsing will succeed
        var m = Complex._format.exec(s);  // Regular expression magic
        return new Complex(parseFloat(m[1]), parseFloat(m[2]));
    } catch (x) {  // And throw an exception if it fails
        throw new TypeError("Can't parse '" + s + "' as a complex number.");
    }
};

Complex._format = /^\{([^,]+),([^}]+)\}$/;

자스가 자바 스타일의 클래스 멤버를 흉내 낼 수 있다고 하더라도, 자바의 중요한 특징 중 자스가 지원하지 않는 것이 몇가지 있다. 먼저 자바는 인스턴스 메서드에서 인스턴스 필드를 메서드의 지역 변수처럼 사용할 수 있고, 이런 필드에 대해 어떠한 접두사도 붙일 필요가 없다. 자스는 이를 지원하지 않지만 with 구문으로 비슷한 효과를 얻을수있다.(권장x)

자바는 final을 사용하여 상수 필드를 정의할 수 있다. 그리고 클래스 내부에서만 사용하고 외부에서 볼 수 없는 필드나 메서드는 private으로 정의할 수 있다. 자스에서는 힌트를 제공하는 표기 규칙을 사용한다. 예를들어 값이 변경되면 안 되는 프로퍼티들은 이름이 대문자(const 나왔네 이제는)이고 밑줄로 시작하는 이름의 프로퍼티는 클래스 외부에서 사용하면 안된다는 뜻이다.

private 프로퍼티는 클로저 상의 지역 변수로 흉내 낼 수 있고, 상수 프로퍼티는 ECMAScript 5에서 사용할 수 있다.

클래스 확장하기

자스의 프로토타입 기반 상속 매커니즘은 동적이다. 객체는 자신의 프로토타입에서 프로퍼티를 상속받는데, 심지어 이는 개체가 생성된 이후에 프로토타입이 변경되더라도 마찬가지다. 다시 말해 자스 객체의 프로토타입에 메서드를 추가함으로써 간단히 자스 클래스를 확장할 수 있다는 뜻이다.

Object.prototype에도 메서드를 추가할 수 있고, 그러면 모든 객체에서 추가된 메서드를 사용할 수 있다. 그러난 이는 권장되지 않는다.(ECMAScript5 이전 버전에서는 이러한 추가 메서드가 열거되지 않게 할 방법이 없기 때문)

클래스와 형식

자스 핵심부의 내장 객체들은 classof() 함수와 같은 코드를 사용하여, 클래스 속성으로 구별할 수 있다. 하지만, 자신만의 클래스를 정의하면 인스턴스 객체의 class 속성은 언제나 Object이다. 객체의 클래스를 판단하는 세 가지 기법

instanceof 연산자, constructor 프로퍼티, 생성자 함수 이름.  but 어떤 것도 만족스럽지 않다

덕 타이핑--객체의 클래스가 무엇인지보다는 객체가 무엇을 하느냐에 더 중점을 두는 프로그래밍 철학

instanceof 연산자

4장에서 나왔듯이 왼쪽 피연산자는 클래스를 판별하려는 객체이며, 오른쪽 피연산자는 생성자 함수(클래스 이름)이다.

o instanceof c : 만약 o가 c.prototype을 상속한다면 true

이러한 상속은 직접적일 필요가 없고 o가 c.prototype을 상속한 어떤 객체를 상속한다고해도 true

생성자는 클래스를 구별하는 대표 수단으로서의 역할을 하지만, 클래스 구별에서의 핵심은 프로토타입이다.

그런데 instanceof는 객체를 생성하는 데 어떤 생성자를 사용했는지를 테스트하지는 않는다.

만약 어떤 객체의 프로토타입 체인에 특별한 프로토타입 객체가 있는지를 검사하려 하고 생성자 함수를 검사의 기준으로 삼고 싶지 않다면, isPrototype() 메서드를 대신 사용할 수 있다.

range 클래스가 객체 r의 프로토타입인지를 알고 싶다면

range.methods.isPrototypeOf(r);  //range.methods는 프로토타입 객체다

instanceof 연산자와 isPrototypeOf() 메서드의 한 가지 단점은 이들은 오직 어떤 객체와 우리와 지정한 클래스에 대해서만 테스트를 할 뿐, 어떤 객체의 클래스가 무엇인지 질의할 수는 없다는 것이다.

더 심각한 단점은 둘 이상의 창이나 프레임을 사용하는 웹 애플리케이션의 클라이언트 측 자스에서 나타난다. 각 창이나 프레임은 서로 구분되는 실행 컨텍스트이고, 각각 자신만의 전역 객체와 생성자 함수의 집합이 있다. 서로 다른 두 프레임에 생성된 두 배열은, 이름은 같지만 실제로는 별개의 프로토타입 객체를 상속한다. 즉 하나의 프레임에 만들어진 배열은 다른 프레임에 있는 Array() 생성자의 인스턴스가 아니다.

constructor 프로퍼티

생성자는 대표적인 클래스 구별 수단이기 때문에, 이는 간단한 방법이다. 이 방법은 instanceof와 같은 문제를 안고 있다. 값을 공유하는 여러 실행 컨텍스트가 있을 때, 이 기법은 작동하지 않을 것이다. 또한 자스에서 모든 객체가 constructor 프로퍼티를 가지고 있지는 않다.

생성자 이름

객체의 클래스를 구별하는 데 생성자 이름을 사용하는 기법은 constructor 프로퍼티 자체를 사용하는 것과 같은 문제를 안고 있는데, 이는 모든 객체가 constructor 프로퍼티를 가지고 있지는 않기 때문이다. 게다가 모든 함수가 이름을 가진 것도 아니다.

function type(o) {
    var t, c, n;  // type, class, name

    if (o === null) return "null";

    if (o !== o) return "nan";

    if ((t = typeof o) !== "object") return t;

    if ((c = classof(o)) !== "Object") return c;

    if (o.constructor && typeof o.constructor === "function" &&
        (n = o.constructor.getName())) return n;

    return "Object";
}

function classof(o) {
    return Object.prototype.toString.call(o).slice(8,-1);
};
    
Function.prototype.getName = function() {
    if ("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

덕 타이핑

앞에 객체 클래스 판별 기법 중 문제가 없는 것은 없었다. 대안은 이 객체의 클래스가 무엇인가를 묻는 대신 이 객체가 무엇을 할 수있는가를 묻는 것이다.

덕 타이핑  새 한마리가 오리처럼 걷고 오리처럼 헤엄치고 오리처럼 꽥꽥거리면, 그 새는 오리임이 틀림없다.

 

덕 타이핑에 대한 한가지 접근 방식은 무간섭주의이다. 우리는 우리의 입력 객체가 필요한 메서드를 구현하고 있다고 가정하고, 이 입력 객체가 메서드를 실제로 구현하고 있는지에 대해서는 전혀 검사하지 않는다.

quacks() 함수는 어떤 객체(첫 번째 인자)가, 지정된 메서드(나머지 인자)를 구현하고 있는지를 검사한다. 두 번째 이후의 각 인자에 대해, 인자가 문자열이라면 메서드의 이름이 문자열과 같은지를 검사할 것이다. 만약 인자가 객체라면 이 객체가 가진 메서드와 같은 이름을 가진 메서드를, 첫 번째 인자로 주어진 객체도 구현하고 있는지를 검사한다. 만약 인자가 함수라면, 이 함수를 생성자 함수로 간주하고 생성자 함수의 프로토타입 객체와 첫 번째 인자의 객체가 서로 같은 함수를 구현하고 있는지를 검사한다.

function quacks(o /*, ... */) {
    for(var i = 1; i < arguments.length; i++) {  // for each argument after o
        var arg = arguments[i];
        switch(typeof arg) { // If arg is a:
        case 'string':       // string: check for a method with that name
            if (typeof o[arg] !== "function") return false;
            continue;
        case 'function':     // function: use the prototype object instead
            arg = arg.prototype
        case 'object':       // object: check for matching methods
            for(var m in arg) { // For each property of the object
                if (typeof arg[m] !== "function") continue; // skip non-methods
                if (typeof o[m] !== "function") return false;
            }
        }
    }
    
    return true;
}

자바스크립트의 객체 지향 기법

예제: Set 클래스

세트는 중복되지 않은 값을 정렬되지 않은 형태로 저장하는 데이터 구조다. 세트의 필수 연산은 어떤 값이 이미 세트에 있는지를 검사하는 것과 값을 세트에 추가하는 것이고, 일반적으로 이 연산은 빠르게 수행되도록 구현된다. 자스 객체는 기본적으로 프로퍼티 이름으로 이루어진 세트다. 따라서 객체를 문자열의 세트로 사용하는 것은 큰 일이 아니다.

function Set() {          // This is the constructor
    this.values = {};     // The properties of this object hold the set
    this.n = 0;           // How many values are in the set
    this.add.apply(this, arguments);  // All arguments are values to add
}

// Add each of the arguments to the set.
Set.prototype.add = function() {
    for(var i = 0; i < arguments.length; i++) {  // For each argument
        var val = arguments[i];                  // The value to add to the set
        var str = Set._v2s(val);                 // Transform it to a string
        if (!this.values.hasOwnProperty(str)) {  // If not already in the set
            this.values[str] = val;              // Map string to value
            this.n++;                            // Increase set size
        }
    }
    return this;                                 // Support chained method calls
};

// Remove each of the arguments from the set.
Set.prototype.remove = function() {
    for(var i = 0; i < arguments.length; i++) {  // For each argument
        var str = Set._v2s(arguments[i]);        // Map to a string
        if (this.values.hasOwnProperty(str)) {   // If it is in the set
            delete this.values[str];             // Delete it
            this.n--;                            // Decrease set size
        }
    }
    return this;                                 // For method chaining
};

// Return true if the set contains value; false otherwise.
Set.prototype.contains = function(value) {
    return this.values.hasOwnProperty(Set._v2s(value));
};

// Return the size of the set.
Set.prototype.size = function() { return this.n; };

// Call function f on the specified context for each element of the set.
Set.prototype.foreach = function(f, context) {
    for(var s in this.values)                 // For each string in the set
        if (this.values.hasOwnProperty(s))    // Ignore inherited properties
            f.call(context, this.values[s]);  // Call f on the value
};

// This internal function maps any JavaScript value to a unique string.
Set._v2s = function(val) {
    switch(val) {
        case undefined:     return 'u';          // Special primitive
        case null:          return 'n';          // values get single-letter
        case true:          return 't';          // codes.
        case false:         return 'f';
        default: switch(typeof val) {
            case 'number':  return '#' + val;    // Numbers get # prefix.
            case 'string':  return '"' + val;    // Strings get " prefix.
            default: return '@' + objectId(val); // Objs and funcs get @
        }
    }

    // For any object, return a string. This function will return a different
    // string for different objects, and will always return the same string
    // if called multiple times for the same object. To do this it creates a
    // property on o. In ES5 the property would be nonenumerable and read-only.
    function objectId(o) {
        var prop = "|**objectid**|";   // Private property name for storing ids
        if (!o.hasOwnProperty(prop))   // If the object has no id
            o[prop] = Set._v2s.next++; // Assign it the next available
        return o[prop];                // Return the id
    }
};
Set._v2s.next = 100;    // Start assigning object ids at this value.

예제 : 열거형

ECMAScript 5에서 enum은 예약어지만 실제로 사용되지는 않는데, 이는 자스가 언젠가는 내장 열거형을 지원할 가능성을 열어둔 것이다. 예제는 단일 enumeration() 함수로 구성되어 있다. 그러나 이 함수는 생성자 함수가 아니고 enumeration 클래스를 정의하지도 않는다. 이 함수는 팩터리 함수로, 호출될 때마다 새 클래스를 생성하고 반환한다.

// This function creates a new enumerated type.  The argument object specifies
// the names and values of each instance of the class. The return value
// is a constructor function that identifies the new class.  Note, however
// that the constructor throws an exception: you can't use it to create new
// instances of the type.  The returned constructor has properties that 
// map the name of a value to the value itself, and also a values array,
// a foreach() iterator function
function enumeration(namesToValues) {
    // This is the dummy constructor function that will be the return value.
    var enumeration = function() { throw "Can't Instantiate Enumerations"; };

    // Enumerated values inherit from this object.
    var proto = enumeration.prototype = {
        constructor: enumeration,                   // Identify type
        toString: function() { return this.name; }, // Return name
        valueOf: function() { return this.value; }, // Return value
        toJSON: function() { return this.name; }    // For serialization
    };

    enumeration.values = [];  // An array of the enumerated value objects

    // Now create the instances of this new type.
    for(name in namesToValues) {         // For each value 
        var e = inherit(proto);          // Create an object to represent it
        e.name = name;                   // Give it a name
        e.value = namesToValues[name];   // And a value
        enumeration[name] = e;           // Make it a property of constructor
        enumeration.values.push(e);      // And store in the values array
    }
    // A class method for iterating the instances of the class
    enumeration.foreach = function(f,c) {
        for(var i = 0; i < this.values.length; i++) f.call(c,this.values[i]);
    };

    // Return the constructor that identifies the new type
    return enumeration;
}

 

열거형으로 카드 나타내기

// Define a class to represent a playing card
function Card(suit, rank) {
    this.suit = suit;         // Each card has a suit
    this.rank = rank;         // and a rank
}

// These enumerated types define the suit and rank values
Card.Suit = enumeration({Clubs: 1, Diamonds: 2, Hearts:3, Spades:4});
Card.Rank = enumeration({Two: 2, Three: 3, Four: 4, Five: 5, Six: 6,
                         Seven: 7, Eight: 8, Nine: 9, Ten: 10,
                         Jack: 11, Queen: 12, King: 13, Ace: 14});

// Define a textual representation for a card
Card.prototype.toString = function() {
    return this.rank.toString() + " of " + this.suit.toString();
};
// Compare the value of two cards as you would in poker
Card.prototype.compareTo = function(that) {
    if (this.rank < that.rank) return -1;
    if (this.rank > that.rank) return 1;
    return 0;
};

// A function for ordering cards as you would in poker
Card.orderByRank = function(a,b) { return a.compareTo(b); };

// A function for ordering cards as you would in bridge 
Card.orderBySuit = function(a,b) {
    if (a.suit < b.suit) return -1;
    if (a.suit > b.suit) return 1;
    if (a.rank < b.rank) return -1;
    if (a.rank > b.rank) return  1;
    return 0;
};


// Define a class to represent a standard deck of cards
function Deck() {
    var cards = this.cards = [];     // A deck is just an array of cards
    Card.Suit.foreach(function(s) {  // Initialize the array
                          Card.Rank.foreach(function(r) {
                                                cards.push(new Card(s,r));
                                            });
                      });
}
 
// Shuffle method: shuffles cards in place and returns the deck
Deck.prototype.shuffle = function() { 
    // For each element in the array, swap with a randomly chosen lower element
    var deck = this.cards, len = deck.length;
    for(var i = len-1; i > 0; i--) {
        var r = Math.floor(Math.random()*(i+1)), temp;     // Random number
        temp = deck[i], deck[i] = deck[r], deck[r] = temp; // Swap
    }
    return this;
};

// Deal method: returns an array of cards
Deck.prototype.deal = function(n) {  
    if (this.cards.length < n) throw "Out of cards";
    return this.cards.splice(this.cards.length-n, n);
};

// Create a new deck of cards, shuffle it, and deal a bridge hand
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);

표준 변환 메서드

1. 가장 중요한 메서드는 toString()이다. 이 메서드의 목적은 객체의 문자열 표현을 반환하는 것이다. 만약 이 메서드를 구현하지 않으면, 클래스는 Object.prototype의 기본 구현을 상속하고, 기본 구현 메서드는 별 쓸모없는 문자열 "[object Object]"을 반환할 것이다. toString() 메서드는 프로그램 사용자에게 사람이 적절히 읽을 수 있는 문자열을 반환해야 한다.

2. toLocaleString()은 toString()과 밀접한 연관이 있다. toLocaleString() 메서드는 객체를 로케일 문자열로 변환한다.

3. valueOf() 메서드는 객체를 원시 값으로 변환한다. 예를들면, valueOf() 메서드는 객체가 숫자 컨텍스트에서 산술연산자(+제외)나 비교 연산자와 함께 사용될 때 자동으로 호출된다. 거의 필요없지만 앞의 열거형은 필요하다.

4. toJSON() 메서드는 JSON.stringify()에 의해 자동으로 호출된다. 만약 Range 객체나 Complex 객체에 대해 JSON.stringify()를 호출하면 {"from":1,"to":3}을 얻는다. 만약 이 문자열을 JSON.parse()에 전달하면, Range 객체와 Complex 객체에 대해 적절한 프로퍼티를 가진 일반 객체를 얻겠지만 이 객체가 Range와 Complex 객체의 메서드를 상속하고 있지는 않다. Date 객체의 toJSON() 메서드를 생각하면 된다.

비교 메서드

자스 동등 연산자들은 객체를 비교할 때, 값이 아니라 참조를 사용한다. 즉, 주어진 두 참조가 같은 객체를 가리키고 있는지를 살펴본다. 이런 연산자들은 두 객체에, 이름이 같고 값도 같은 프로퍼티가 존재하는지를 검사하지 않는다.

ex) Range 클래스

Range.prototype.constructor=Range;
Range.prototype.equals=function(that){
    if(that==null) return false;
    if(that.constructor !==Range) return false;
    return this.from==thiat.from&&this.to==that.to;
}

Set 클래스

Set.prototype.equals=function(that){
    if(this===that) return true;
    if(!(that instanceof Set)) return false;
    if(this.size()!=that.size()) return false;
    try{
        this.foreach(function(v){if(!that.contains(v)) throw false; });
        return true;
    } catch(x){
      if(x===false) return false;
      throw x;
    }
};
    

 

객체를 어떤 순서에 따라 비교하는 것도 유용하다. Range 객체는 정렬이 필요하고 Set 객체는 정렬이 필요하지 않다.

자스의 관계연산자(<)에 객체를 사용하려면 valueOf() 메서드를 호출하는데 대부분 없기 때문에 comapreTo() 메서드를 정의해야 한다. 이 메서드는 하나의 인자를 받고, 메서드를 호출한 객체와 인자를 비교한다. 만약 this 객체가 인자 객체보다 크면 0보다 큰 값을 반환하고 같으면 0,을 반환해야 한다.

메서드 빌려오기

자스의 메서드에 어떤 특별한 것이 있지는 않다. 메서드는 단순히 객체의 프로퍼티로 할당된 함수이고, 객체를 통해 또는 객체 상에서 호출된다. 하나의 함수는 두 프로퍼티에 할당될 수 있고, 그런 다음에는 두개의 메서드로 작동한다. 전통적인 객체 지향 언어의 관점에서 자스를 바라보면, 어떤 클래스의 메서드를 다른 클래스의 메서드로 사용하는 것이 다중 상속의 한 형태로 생각될 수 있다. 그러나 자스는 전통적인 객체 지향 언어가 아니고, 이런 재활용 방식을 메서드 빌려오기라고 부르겠다.

var generic = {
    // Returns a string that includes the name of the constructor function
    // if available and the names and values of all noninherited, nonfunction
    // properties.
    toString: function() {
        var s = '[';
        // If the object has a constructor and the constructor has a name,
        // use that class name as part of the returned string.  Note that
        // the name property of functions is nonstandard and not supported
        // everywhere.
        if (this.constructor && this.constructor.name)
            s += this.constructor.name + ": ";

        // Now enumerate all noninherited, nonfunction properties
        var n = 0;
        for(var name in this) {
            if (!this.hasOwnProperty(name)) continue;   // skip inherited props
            var value = this[name];
            if (typeof value === "function") continue;  // skip methods
            if (n++) s += ", ";
            s += name + '=' + value;
        }
        return s + ']';
    },

    // Tests for equality by comparing the constructors and instance properties
    // of this and that.  Only works for classes whose instance properties are
    // primitive values that can be compared with ===.
    // As a special case, ignore the special property added by the Set class.
    equals: function(that) {
        if (that == null) return false;
        if (this.constructor !== that.constructor) return false;
        for(var name in this) {
            if (name === "|**objectid**|") continue;     // skip special prop.
            if (!this.hasOwnProperty(name)) continue;    // skip inherited 
            if (this[name] !== that[name]) return false; // compare values
        }
        return true;  // If all properties matched, objects are equal.
    }
};

private 상태

전통적 객체 지향 프로그래밍에서는 중요한 상태 변수를 직접 읽거나 쓰는 것을 금지하고, 오직 객체의 메서드만을 통해 객체 상태에 접근하도록 허용함으로써 객체의 상태를 캡슐화하거나 숨기는 경우가 있다. 이 같은 목적을 위해 자바는 private 인스턴스 필드를 선언하며, 이 private 필드는 오직 인스턴스 메서드로만 접근할 수 있고 클래스 외부에는 보이지 않는다. 

인스턴스를 생성할 때, 생성자 호출의 클로저에 포착된 변수를 사용함으로써 private 인스턴스 필드를 자바와 거의 유사하게 사용할 수 있다. 이를 위해 생성자 내부에 함수들을 선언하고, 이 함수들을 새로 생성한 객체의 프로퍼티로 할당한다.

function Range(from, to) {
    // Don't store the endpoints as properties of this object. Instead
    // define accessor functions that return the endpoint values.
    // These values are stored in the closure.
    this.from = function() { return from; };
    this.to = function() { return to; };
}

// The methods on the prototype can't see the endpoints directly: they have
// to invoke the accessor methods just like everyone else.
Range.prototype = {
    constructor: Range,
    includes: function(x) { return this.from() <= x && x <= this.to(); },
    foreach: function(f) {
        for(var x=Math.ceil(this.from()), max=this.to(); x <= max; x++) f(x);
    },
    toString: function() { return "(" + this.from() + "..." + this.to() + ")"; }
};

그러나 ECMAScript5의 기능을 사용하지 않는 한, from 프로퍼티와 to 프로퍼티는 여전히 덮어 쓰일 수 있고, 따라서 Range 객체가 실제로 불변인 것은 아니다.

이러한 캡슐화 기법에는 오버헤드가 있다는 것을 염두해 두어라. 상대를 캡슐화하도록 클로저를 사용하는 클래스는 그렇지 않은 클래스보다 확실히 느리고 크다.

생성자 오버로딩과 팩터리 메서드

때로는 객체를 한 가지 이상의 방법으로 초기화해야 할 필요가 있다. 이를 위한 한 가지 방법은 생성자를 오버로드하고, 전달인자에 따라 초기화를 수행하는 것이다. 그러나 이러한 생성자에는 모호함이 있는데, 멤버가 하나만 있고 그 하나의 멤버가 배열인 세트는 이 생서자를 사용하여 생성할 수 없다.

 

팩터리 메서드의 매력은 메서드 이름을 어떤 것이든 원하는 대로 지정할 수 있다는 점이고, 이름이 다른 메서드는 다른 초기화를 수행하도록 할 수 잇다.

Complex.polar=function(r,theta){
    return new Complex(r*Math.cos(theta), r*Math.sin(theta));
}

 

Set.fromArray=function(a){
    s=new Set();  //빈 세트를 만든다.
    s.add.apply(s,a); //배열 a의 ㅇ소를 add 메서드에 전달한다.
    return s; //세트를 반환한다.
};

서브 클래스

객체 지향 프로그래밍에서 클래스 B는 다른 클래스 A를 확장하거나 클래스 A의 하위 분류가 될 수 있다. 이런 경우, 클래스 A를 수퍼 클래스라 하고 클래스 B를 서브 클래스라고 한다. 클래스 B의 인스턴스는 클래스 A의 모든 인스턴스 메서드를 상속한다. 클래스 B는 자신만의 인스턴스 메서드를 가질 수 있고, 그중 몇가지는 클래스 A로부터 상속받은 메서드를 똑같은 이름으로 재정의할 수도 잇다. 클래스 B의 메서드가 클래스 A의 메서드를 재정의했을 때, 클래스 B의 재정의된 메서드에서 클래스 A의 원래 메서드를 호출할 수가 있는데 이를 메서드 체이닝 이라고 한다. 비슷하게 서브 클래스의 생성자B가 수퍼클래스의 생성자 A를 호출할 필요가 있는데, 이는 생성자 체이닝이라고 한다.

서브 클래스 정의

자스 객체는 클래스의 프로토타입 객체로부터 프로퍼티(보통은 메서드)를 상속한다. 만약 객체 O가 클래스 B의 인스턴스이고 클래스 B가 클래스 A의 서브 클래스라면, 객체 O는 틀림없이 클래스 A의 프로퍼티 또한 상속하고 있다.

B.prototype=inherit(A.prototype);  //서브 클래스는 수퍼 클래스를 상속한다.

B.prototype.constructor=B;          //상속된 constructor 프로퍼티를 재정의한다.

이 두줄의 코드는 자스에서 서브 클래스를 만드는 핵심이다.

ex) 서브 클래스 정의 유틸리티

// A simple function for creating simple subclasses
function defineSubclass(superclass,  // Constructor of the superclass
                        constructor, // The constructor for the new subclass
                        methods,     // Instance methods: copied to prototype
                        statics)     // Class properties: copied to constructor
{
    // Set up the prototype object of the subclass
    constructor.prototype = inherit(superclass.prototype);
    constructor.prototype.constructor = constructor;
    // Copy the methods and statics as we would for a regular class
    if (methods) extend(constructor.prototype, methods);
    if (statics) extend(constructor, statics);
    // Return the class
    return constructor;
}

// We can also do this as a method of the superclass constructor
Function.prototype.extend = function(constructor, methods, statics) {
    return defineSubclass(this, constructor, methods, statics);
};

ex) SingletonSet:간단한 세트 서브 클래스

// The constructor function 
function SingletonSet(member) {
    this.member = member;   // Remember the single member of the set
}

// Create a prototype object that inherits from the prototype of Set.
SingletonSet.prototype = inherit(Set.prototype);

// Now add properties to the prototype.
// These properties override the properties of the same name from Set.prototype.
extend(SingletonSet.prototype, {
           // Set the constructor property appropriately
           constructor: SingletonSet,
           // This set is read-only: add() and remove() throw errors
           add: function() { throw "read-only set"; },    
           remove: function() { throw "read-only set"; }, 
           // A SingletonSet always has size 1
           size: function() { return 1; },                
           // Just invoke the function once, passing the single member.
           foreach: function(f, context) { f.call(context, this.member); },
           // The contains() method is simple: true only for one value
           contains: function(x) { return x === this.member; }
       });

 

SingletonSet은 Set의 메서드를 정적으로 빌려오지 않는다는 점을 유념하라. SingletonSet은 동적으로 Set 클래스의 메서드를 상속한다. 만약 Set.prototype에 새 메서드를 추가하면 그 즉시 Set과 SingletonSet의 모든 인스턴스에서 추가한 메서드를 사용할 수 있다.

생성자 체이닝과 메서드 체이닝

서브 클래스를 정의할 때 종종 메서드를 완전히 교체하지 않고, 오직 수퍼 클래스 메서드의 행위를 확장하거나 수정만 하고 싶을때가 있다. 이를 위해 서브 클래스의 생성자와 메서드는 수퍼 클래스의 생성자와 메서드를 호출하거나 체이닝한다.

ex) 수퍼 클래스로의 생성자 체이닝과 메서드 체이닝

/*
 * NonNullSet is a subclass of Set that does not allow null and undefined
 * as members of the set.
 */
function NonNullSet() {
    // Just chain to our superclass.
    // Invoke the superclass constructor as an ordinary function to initialize
    // the object that has been created by this constructor invocation.
    Set.apply(this, arguments);
}

// Make NonNullSet a subclass of Set:
NonNullSet.prototype = inherit(Set.prototype);
NonNullSet.prototype.constructor = NonNullSet;

// To exclude null and undefined, we only have to override the add() method
NonNullSet.prototype.add = function() {
    // Check for null or undefined arguments
    for(var i = 0; i < arguments.length; i++)
        if (arguments[i] == null)
            throw new Error("Can't add null or undefined to a NonNullSet");

    // Chain to the superclass to perform the actual insertion
    return Set.prototype.add.apply(this, arguments);
};

ex)클래스 팩터리와 메서드 체이닝

/*
 * This function returns a subclass of specified Set class and overrides 
 * the add() method of that class to apply the specified filter.
 */
function filteredSetSubclass(superclass, filter) {
    var constructor = function() {          // The subclass constructor
        superclass.apply(this, arguments);  // Chains to the superclass
    };
    var proto = constructor.prototype = inherit(superclass.prototype);
    proto.constructor = constructor;
    proto.add = function() {
        // Apply the filter to all arguments before adding any
        for(var i = 0; i < arguments.length; i++) {
            var v = arguments[i];
            if (!filter(v)) throw("value " + v + " rejected by filter");
        }
        // Chain to our superclass add implementation
        superclass.prototype.add.apply(this, arguments);
    };
    return constructor;
}

위에서 흥미로운 점은 서브 클래스 생성 코드를 함수로 래핑함으로써, 서브 클래스 생성자에게 superclass 인자를 사용할 수 있고 실제 수퍼 클래스 이름을 하드 코딩하는 대신 메서드 체이닝 코드를 사용할 수 있다는 점이다. 이는 만약 여러분이 수퍼 클래스를 변경하고 싶다면 코드에서 수퍼 클래스가 나오는 모든 위치를 찾아 바꾸지 않고, 한 부분만 변경하면 된다는 뜻이다.

클래스 팩터리!!  다시 볼때 다시 공부

조합 대 서브 클래스

지금까지는 특정 조건에 따라 그 멤버를 제한하는 세트를 정의하려 했었고, 이를 위해 특정 필터 함수를 사용하여 세트의 멤버 자격을 제한하는 세트 서브 클래스를 만들었다. 이 경우, 수퍼 클래스는 하나이더라도 필터의 종류가 달라지면 새로운 클래스를 생성해야 한다.

그러나 좀더 나은 방법이 있다. 객체 지향 설계에서 잘 알려진 원칙으로 상속보다는 조합을 선호하라 라는 말이 있다.

/*
 * A FilteredSet wraps a specified set object and applies a specified filter
 * to values passed to its add() method.  All of the other core set methods 
 * simply forward to the wrapped set instance.
 */
var FilteredSet = Set.extend(
    function FilteredSet(set, filter) {  // The constructor
        this.set = set;
        this.filter = filter;
    }, 
    {  // The instance methods
        add: function() {
            // If we have a filter, apply it
            if (this.filter) {
                for(var i = 0; i < arguments.length; i++) {
                    var v = arguments[i];
                    if (!this.filter(v))
                        throw new Error("FilteredSet: value " + v +
                                        " rejected by filter");
                }
            }

            // Now forward the add() method to this.set.add()
            this.set.add.apply(this.set, arguments);
            return this;
        },
        // The rest of the methods just forward to this.set and do nothing else.
        remove: function() {
            this.set.remove.apply(this.set, arguments);
            return this;
        },
        contains: function(v) { return this.set.contains(v); },
        size: function() { return this.set.size(); },
        foreach: function(f,c) { this.set.foreach(f,c); }
    });

이렇게 조합을 사용했을 때의 이점은 오직 하나의 FilteredSet 서브 클래스만 있으면 된다는 것이다. FilteredSet 클래스의 인스턴스는 다른 세트 인스턴스의 멤버 자격을 제한하는 용도로 만들어질 수 있다. 

var s=new FilteredSet(new Set(), function(x){return x!==null;});

var t=new FilteredSet(s,{function(x}{return !(x instanceof Set);});

클래스 계층 구조와 추상 클래스

아래는 추상 세트 클래스의 계층 구조를 정의한다. 앞의 Funcction.prototype.extends()를 사용함

// A convenient function that can be used for any abstract method
function abstractmethod() { throw new Error("abstract method"); }

/*
 * The AbstractSet class defines a single abstract method, contains().
 */
function AbstractSet() { throw new Error("Can't instantiate abstract classes");}
AbstractSet.prototype.contains = abstractmethod;

/*
 * NotSet is a concrete subclass of AbstractSet.
 * The members of this set are all values that are not members of some
 * other set. Because it is defined in terms of another set it is not
 * writable, and because it has infinite members, it is not enumerable.
 * All we can do with it is test for membership.
 * Note that we're using the Function.prototype.extend() method we defined
 * earlier to define this subclass.
 */
var NotSet = AbstractSet.extend(
    function NotSet(set) { this.set = set; },
    {
        contains: function(x) { return !this.set.contains(x); },
        toString: function(x) { return "~" + this.set.toString(); },
        equals: function(that) {
            return that instanceof NotSet && this.set.equals(that.set);
        }
    }
);


/*
 * AbstractEnumerableSet is an abstract subclass of AbstractSet.
 * It defines the abstract methods size() and foreach(), and then implements
 * concrete isEmpty(), toArray(), to[Locale]String(), and equals() methods
 * on top of those. Subclasses that implement contains(), size(), and foreach() 
 * get these five concrete methods for free.
 */
var AbstractEnumerableSet = AbstractSet.extend(
    function() { throw new Error("Can't instantiate abstract classes"); }, 
    {
        size: abstractmethod,
        foreach: abstractmethod,
        isEmpty: function() { return this.size() == 0; },
        toString: function() {
            var s = "{", i = 0;
            this.foreach(function(v) {
                             if (i++ > 0) s += ", ";
                             s += v;
                         });
            return s + "}";
        },
        toLocaleString : function() {
            var s = "{", i = 0;
            this.foreach(function(v) {
                             if (i++ > 0) s += ", ";
                             if (v == null) s += v; // null & undefined
                             else s += v.toLocaleString(); // all others
                         });
            return s + "}";
        },
        toArray: function() {
            var a = [];
            this.foreach(function(v) { a.push(v); });
            return a;
        },
        equals: function(that) {
            if (!(that instanceof AbstractEnumerableSet)) return false;
            // If they don't have the same size, they're not equal
            if (this.size() != that.size()) return false;
            // Now check whether every element in this is also in that.
            try {
                this.foreach(function(v) {if (!that.contains(v)) throw false;});
                return true;  // All elements matched: sets are equal.
            } catch (x) {
                if (x === false) return false; // Sets are not equal
                throw x; // Some other exception occurred: rethrow it.
            }
        }
    });

/*
 * SingletonSet is a concrete subclass of AbstractEnumerableSet.
 * A singleton set is a read-only set with a single member.
 */
var SingletonSet = AbstractEnumerableSet.extend(
    function SingletonSet(member) { this.member = member; },
    {
        contains: function(x) {  return x === this.member; },
        size: function() { return 1; },
        foreach: function(f,ctx) { f.call(ctx, this.member); }
    }
);


/*
 * AbstractWritableSet is an abstract subclass of AbstractEnumerableSet.
 * It defines the abstract methods add() and remove(), and then implements
 * concrete union(), intersection(), and difference() methods on top of them.
 */
var AbstractWritableSet = AbstractEnumerableSet.extend(
    function() { throw new Error("Can't instantiate abstract classes"); }, 
    {
        add: abstractmethod,
        remove: abstractmethod,
        union: function(that) {
            var self = this;
            that.foreach(function(v) { self.add(v); });
            return this;
        },
        intersection: function(that) {
            var self = this;
            this.foreach(function(v) { if (!that.contains(v)) self.remove(v);});
            return this;
        },
        difference: function(that) {
            var self = this;
            that.foreach(function(v) { self.remove(v); });
            return this;
        }
    });

/*
 * An ArraySet is a concrete subclass of AbstractWritableSet.
 * It represents the set elements as an array of values, and uses a linear
 * search of the array for its contains() method. Because the contains()
 * method is O(n) rather than O(1), it should only be used for relatively
 * small sets. Note that this implementation relies on the ES5 Array methods
 * indexOf() and forEach().
 */
var ArraySet = AbstractWritableSet.extend(
    function ArraySet() {
        this.values = [];
        this.add.apply(this, arguments);
    },
    {
        contains: function(v) { return this.values.indexOf(v) != -1; },
        size: function() { return this.values.length; },
        foreach: function(f,c) { this.values.forEach(f, c); },
        add: function() { 
            for(var i = 0; i < arguments.length; i++) {
                var arg = arguments[i];
                if (!this.contains(arg)) this.values.push(arg);
            }
            return this;
        },
        remove: function() {
            for(var i = 0; i < arguments.length; i++) {
                var p = this.values.indexOf(arguments[i]);
                if (p == -1) continue;
                this.values.splice(p, 1);
            }
            return this;
        }
    }
);

 

ECMAScript5 클래스

열거되지 않은 프로퍼티 만들기

이전에 Set 클래스는 어떤 객체를 세트의 멤버로 저장하기 위해 세트에 추가되는 모든 객체에 object id를 추가하는 트릭을 사용했다. 만약 나중에 다른 코드가 for/in 루프에서 이 객체를 사용하면 object id 프로퍼티도 열거될 것이다. 이를 막는 방법을 소개한다.

// Wrap our code in a function so we can define variables in the function scope
(function() { 
     // Define objectId as a nonenumerable property inherited by all objects.
     // When this property is read, the getter function is invoked.
     // It has no setter, so it is read-only.
     // It is nonconfigurable, so it can't be deleted.
     Object.defineProperty(Object.prototype, "objectId", {
                               get: idGetter,       // Method to get value
                               enumerable: false,   // Nonenumerable
                               configurable: false  // Can't delete it
                           });

     // This is the getter function called when objectId is read
     function idGetter() {             // A getter function to return the id
         if (!(idprop in this)) {      // If object doesn't already have an id
             if (!Object.isExtensible(this)) // And if we can add a property
                 throw Error("Can't define id for nonextensible objects");
             Object.defineProperty(this, idprop, {         // Give it one now.
                                       value: nextid++,    // This is the value
                                       writable: false,    // Read-only
                                       enumerable: false,  // Nonenumerable
                                       configurable: false // Nondeletable
                                   });
         }
         return this[idprop];          // Now return the existing or new value
     };

     // These variables are used by idGetter() and are private to this function
     var idprop = "|**objectId**|";    // Assume this property isn't in use
     var nextid = 1;                   // Start assigning ids at this #

}()); // Invoke the wrapper function to run the code right away

변경되지 않는 클래스 정의하기

 

ex) 읽기 전용 프로퍼티와 메서드를 가진 변경되지 않는 클래스

// This function works with or without 'new': a constructor and factory function
function Range(from,to) {
    // These are descriptors for the read-only from and to properties.
    var props = {
        from: {value:from, enumerable:true, writable:false, configurable:false},
        to: {value:to, enumerable:true, writable:false, configurable:false}
    };
    
    if (this instanceof Range)                // If invoked as a constructor
        Object.defineProperties(this, props); // Define the properties
    else                                      // Otherwise, as a factory 
        return Object.create(Range.prototype, // Create and return a new
                             props);          // Range object with props
}

// If we add properties to the Range.prototype object in the same way,
// then we can set attributes on those properties.  Since we don't specify
// enumerable, writable, or configurable, they all default to false.
Object.defineProperties(Range.prototype, {
    includes: {
        value: function(x) { return this.from <= x && x <= this.to; }
    },
    foreach: {
        value: function(f) {
            for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
        }
    },
    toString: {
        value: function() { return "(" + this.from + "..." + this.to + ")"; }
    }
});

위에서는 변경할 수 없고 열거되지 않는 프로퍼티를 정의하기 위해 Object.defineProperties()와 Object.create()를 사용했다. 이 메서드들은 강력하지만, 해당 메서드에 전달해야 하는 프로퍼티 디스크립터 객체는 코드를 읽기 어렵게 만들 수 있다. 대안은 이미 정의된 프로퍼티 속성을 수정할 수 있는 유틸리티 함수를 정의하는 것이다.

ex) 프로퍼티 디스크립터 유틸리티

// Make the named (or all) properties of o nonwritable and nonconfigurable.
function freezeProps(o) {
    var props = (arguments.length == 1)              // If 1 arg
        ? Object.getOwnPropertyNames(o)              //  use all props
        : Array.prototype.splice.call(arguments, 1); //  else named props
    props.forEach(function(n) { // Make each one read-only and permanent
        // Ignore nonconfigurable properties
        if (!Object.getOwnPropertyDescriptor(o,n).configurable) return;
        Object.defineProperty(o, n, { writable: false, configurable: false });
    });
    return o;  // So we can keep using it
}

// Make the named (or all) properties of o nonenumerable, if configurable.
function hideProps(o) {
    var props = (arguments.length == 1)              // If 1 arg
        ? Object.getOwnPropertyNames(o)              //  use all props
        : Array.prototype.splice.call(arguments, 1); //  else named props
    props.forEach(function(n) { // Hide each one from the for/in loop
        // Ignore nonconfigurable properties
        if (!Object.getOwnPropertyDescriptor(o,n).configurable) return;
        Object.defineProperty(o, n, { enumerable: false });
    });
    return o;
}

ex)더 간단한 불변 클래스

function Range(from, to) {    // Constructor for an immutable Range class
    this.from = from;
    this.to = to;
    freezeProps(this);        // Make the properties immutable
}

Range.prototype = hideProps({ // Define prototype with nonenumerable properties
    constructor: Range,
    includes: function(x) { return this.from <= x && x <= this.to; },
    foreach: function(f) {for(var x=Math.ceil(this.from);x<=this.to;x++) f(x);},
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
});

 

객체 상태를 캡슐화하기

삭제되거나 변경되지 않는 getter/setter 메서드 프로퍼티를 정의할 수 있다.

// This version of the Range class is mutable but encapsulates its endpoint
// variables to maintain the invariant that from <= to.
function Range(from, to) {
    // Verify that the invariant holds when we're created
    if (from > to) throw new Error("Range: from must be <= to");

    // Define the accessor methods that maintain the invariant
    function getFrom() {  return from; }
    function getTo() {  return to; }
    function setFrom(f) {  // Don't allow from to be set > to
        if (f <= to) from = f;
        else throw new Error("Range: from must be <= to");
    }
    function setTo(t) {    // Don't allow to to be set < from
        if (t >= from) to = t;
        else throw new Error("Range: to must be >= from");
    }

    // Create enumerable, nonconfigurable properties that use the accessors
    Object.defineProperties(this, {
        from: {get: getFrom, set: setFrom, enumerable:true, configurable:false},
        to: { get: getTo, set: setTo, enumerable:true, configurable:false }
    });
}

// The prototype object is unchanged from previous examples.
// The instance methods read from and to as if they were ordinary properties.
Range.prototype = hideProps({
    constructor: Range,
    includes: function(x) { return this.from <= x && x <= this.to; },
    foreach: function(f) {for(var x=Math.ceil(this.from);x<=this.to;x++) f(x);},
    toString: function() { return "(" + this.from + "..." + this.to + ")"; }
});

클래스 확장 막기

 

Object.prototype을 확장하지 못하게 하려면 간단히 

Object.seal(Object.prototype); 을 하면 된다.

또 다른 자스의 동적 기능은 객체의 메서들르 교체(또는 monkey-patch)하는 것이다. 인스턴스 메서드를 읽기 전용으로 만들면 이런 변경을 막을 수 있다. 앞의 freezeProps() 함수. 다른 방법으로는 Object.freeze()를 사용하는 것으로, Object.freeze()는 Object.seal()의 모든 과정을 수행하고 추가로 모든 프로퍼티를 읽기 전용으로 설정하며 또한 재설정할 수 없도록 만든다.

보통은 이렇게 프로토타입 객쳋에 대해 확장을 막을 필요는 없지만, 객체 확장을 막는 것이 유용한 몇 가지 상황이 있다.

서브 클래스와 ECMAScript 5

function StringSet() {
    this.set = Object.create(null);  // Create object with no proto
    this.n = 0;
    this.add.apply(this, arguments);
}

// Note that with Object.create we can inherit from the superclass prototype
// and define methods in a single call. Since we don't specify any of the
// writable, enumerable, and configurable properties, they all default to false.
// Readonly methods makes this class trickier to subclass.
StringSet.prototype = Object.create(AbstractWritableSet.prototype, {
    constructor: { value: StringSet },
    contains: { value: function(x) { return x in this.set; } },
    size: { value: function(x) { return this.n; } },
    foreach: { value: function(f,c) { Object.keys(this.set).forEach(f,c); } },
    add: {
        value: function() {
            for(var i = 0; i < arguments.length; i++) {
                if (!(arguments[i] in this.set)) {
                    this.set[arguments[i]] = true;
                    this.n++;
                }
            }
            return this;
        } 
    },
    remove: {
        value: function() {
            for(var i = 0; i < arguments.length; i++) {
                if (arguments[i] in this.set) {
                    delete this.set[arguments[i]];
                    this.n--;
                }
            }
            return this;
        } 
    }
});

프로퍼티 디스크립터

ex) ECMAScript 5 프로퍼티 유틸리티

/*
 * Define a properties() method in Object.prototype that returns an
 * object representing the named properties of the object on which it
 * is invoked (or representing all own properties of the object, if
 * invoked with no arguments).  The returned object defines four useful 
 * methods: toString(), descriptors(), hide(), and show().
 */
(function namespace() {  // Wrap everything in a private function scope

     // This is the function that becomes a method of all object
     function properties() {
         var names;  // An array of property names
         if (arguments.length == 0)  // All own properties of this
             names = Object.getOwnPropertyNames(this);
         else if (arguments.length == 1 && Array.isArray(arguments[0]))
             names = arguments[0];   // Or an array of names
         else                        // Or the names in the argument list
             names = Array.prototype.splice.call(arguments, 0);

         // Return a new Properties object representing the named properties
         return new Properties(this, names);
     }

     // Make it a new nonenumerable property of Object.prototype.
     // This is the only value exported from this private function scope.
     Object.defineProperty(Object.prototype, "properties", {
         value: properties,  
         enumerable: false, writable: true, configurable: true
     });

     // This constructor function is invoked by the properties() function above.
     // The Properties class represents a set of properties of an object.
     function Properties(o, names) {
         this.o = o;            // The object that the properties belong to
         this.names = names;    // The names of the properties
     }
     
     // Make the properties represented by this object nonenumerable
     Properties.prototype.hide = function() {
         var o = this.o, hidden = { enumerable: false };
         this.names.forEach(function(n) {
                                if (o.hasOwnProperty(n))
                                    Object.defineProperty(o, n, hidden);
                            });
         return this;
     };

     // Make these properties read-only and nonconfigurable
     Properties.prototype.freeze = function() {
         var o = this.o, frozen = { writable: false, configurable: false };
         this.names.forEach(function(n) {
                                if (o.hasOwnProperty(n))
                                    Object.defineProperty(o, n, frozen);
                            });
         return this;
     };

     // Return an object that maps names to descriptors for these properties.
     // Use this to copy properties along with their attributes:
     //   Object.defineProperties(dest, src.properties().descriptors());
     Properties.prototype.descriptors = function() {
         var o = this.o, desc = {};
         this.names.forEach(function(n) {
                                if (!o.hasOwnProperty(n)) return;
                                desc[n] = Object.getOwnPropertyDescriptor(o,n);
                            });
         return desc;
     };

     // Return a nicely formatted list of properties, listing the 
     // name, value and attributes. Uses the term "permanent" to mean
     // nonconfigurable, "readonly" to mean nonwritable, and "hidden"
     // to mean nonenumerable. Regular enumerable, writable, configurable 
     // properties have no attributes listed.
     Properties.prototype.toString = function() {
         var o = this.o; // Used in the nested function below
         var lines = this.names.map(nameToString);
         return "{\n  " + lines.join(",\n  ") + "\n}";
         
         function nameToString(n) {
             var s = "", desc = Object.getOwnPropertyDescriptor(o, n);
             if (!desc) return "nonexistent " + n + ": undefined";
             if (!desc.configurable) s += "permanent ";
             if ((desc.get && !desc.set) || !desc.writable) s += "readonly ";
             if (!desc.enumerable) s += "hidden ";
             if (desc.get || desc.set) s += "accessor " + n
             else s += n + ": " + ((typeof desc.value==="function")?"function"
                                                                   :desc.value);
             return s;
         }
     };

     // Finally, make the instance methods of the prototype object above 
     // nonenumerable, using the methods we've defined here.
     Properties.prototype.properties().hide();
}()); // Invoke the enclosing function as soon as we're done defining it.

 

모듈

 

 

 

 

728x90
LIST

' > 자바스크립트 완벽 가이드' 카테고리의 다른 글

11장 자바스크립트 서브셋과 확장  (0) 2020.11.15
10장 정규 표현식을 사용한 패턴 매칭  (0) 2020.11.14
8장 함수  (0) 2020.11.09
7장 배열  (0) 2020.11.08
6장 객체  (0) 2020.11.07
댓글
공지사항