[javascript] JavaScript로 커스텀 객체를 “적절하게”만드는 방법은 무엇입니까?

속성과 메서드가있는 JavaScript 객체를 만드는 가장 좋은 방법이 무엇인지 궁금합니다.

사람이 사용 var self = this하고 self.모든 기능에서 사용하여 범위가 항상 올바른지 확인하는 예제를 보았습니다 .

그런 다음 .prototype속성을 추가 하는 데 사용 하는 예제를 보았지만 다른 사람들은 인라인으로 수행했습니다.

누군가 속성과 메소드가있는 JavaScript 객체의 적절한 예를 제공 할 수 있습니까?



답변

JavaScript에서 클래스와 인스턴스를 구현하는 데에는 프로토 타입 방식과 클로저 방식의 두 가지 모델이 있습니다. 둘 다 장점과 단점이 있으며 확장 된 변형이 많이 있습니다. 많은 프로그래머와 라이브러리는 언어의 더 추악한 부분을 처리하기 위해 서로 다른 접근 방식과 클래스 처리 유틸리티 기능을 가지고 있습니다.

결과적으로 혼합 회사에서는 메타 클래스가 혼동되어 약간 다르게 동작합니다. 게다가 대부분의 JavaScript 튜토리얼 자료는 끔찍하며 모든 기반을 다루기 위해 어떤 종류의 타협을 제공하여 혼란스러워합니다. (아마도 저자는 혼란 스러울 수 있습니다. JavaScript의 객체 모델은 대부분의 프로그래밍 언어와 매우 다르며, 여러 곳에서 똑바로 잘못 설계되었습니다.)

prototype way로 시작해 봅시다 . 이것은 당신이 얻을 수있는 가장 JavaScript 고유의 것입니다 : 최소한의 오버 헤드 코드가 있으며 instanceof는 이런 종류의 객체의 인스턴스와 작동합니다.

function Shape(x, y) {
    this.x= x;
    this.y= y;
}

이 생성자 함수 new Shapeprototype조회 에 작성하여 작성된 메소드에 메소드를 추가 할 수 있습니다 .

Shape.prototype.toString= function() {
    return 'Shape at '+this.x+', '+this.y;
};

이제 JavaScript가 서브 클래 싱을하는 것을 호출 할 수있는만큼 서브 클래 싱합니다. 우리는 그 이상한 마법 prototype속성 을 완전히 대체하여 그렇게 합니다.

function Circle(x, y, r) {
    Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
    this.r= r;
}
Circle.prototype= new Shape();

메소드를 추가하기 전에 :

Circle.prototype.toString= function() {
    return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}

이 예제는 작동하며 많은 자습서에서 이와 유사한 코드를 볼 수 있습니다. 그러나 사람, 그것은 new Shape()추악합니다. 실제 모양을 만들지 않아도 기본 클래스를 인스턴스화하고 있습니다. 자바 스크립트가 너무 실수이기 때문에이 간단한 경우 작업에 발생합니다 제로 인수하는 경우, 전달 될 수 있도록 x하고 yundefined및 프로토 타입의에 할당 this.x하고 this.y. 생성자 함수가 더 복잡한 작업을 수행하면 얼굴이 평평 해집니다.

따라서 우리가해야 할 일은 기본 클래스의 생성자 함수를 호출하지 않고 클래스 수준에서 원하는 메서드와 다른 멤버를 포함하는 프로토 타입 객체를 만드는 방법을 찾는 것입니다. 이를 위해 헬퍼 코드 작성을 시작해야합니다. 이것이 내가 아는 가장 간단한 접근법입니다.

function subclassOf(base) {
    _subclassOf.prototype= base.prototype;
    return new _subclassOf();
}
function _subclassOf() {};

프로토 타입의 기본 클래스 멤버를 아무것도하지 않는 새로운 생성자 함수로 전송 한 다음 해당 생성자를 사용합니다. 이제 간단히 쓸 수 있습니다 :

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.prototype= subclassOf(Shape);

new Shape()잘못 대신 . 이제 클래스를 빌드하는 데 허용 가능한 기본 세트가 있습니다.

이 모델에서 고려할 수있는 몇 가지 개선 및 확장이 있습니다. 예를 들어 다음은 구문 설탕 버전입니다.

Function.prototype.subclass= function(base) {
    var c= Function.prototype.subclass.nonconstructor;
    c.prototype= base.prototype;
    this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};

...

function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r= r;
}
Circle.subclass(Shape);

두 버전 모두 생성자 함수를 여러 언어로 상속 할 수 없다는 단점이 있습니다. 따라서 서브 클래스가 구성 프로세스에 아무 것도 추가하지 않더라도 기본이 원하는 인수를 사용하여 기본 생성자를 호출해야합니다. 이것은를 사용하여 약간 자동화 할 수 apply있지만 여전히 작성해야합니다.

function Point() {
    Shape.apply(this, arguments);
}
Point.subclass(Shape);

따라서 일반적인 확장은 초기화 항목을 생성자 자체가 아닌 자체 기능으로 나누는 것입니다. 이 함수는 기본에서 잘 상속받을 수 있습니다.

function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!

이제 우리는 각 클래스에 대해 동일한 생성자 함수 상용구를 얻었습니다. 어쩌면 우리는 그것을 자신의 도우미 함수로 옮길 수 있으므로 예를 들어 대신에 입력하고 계속 Function.prototype.subclass돌리지 않아도 기본 클래스의 함수가 서브 클래스를 뱉어 낼 필요가 없습니다.

Function.prototype.makeSubclass= function() {
    function Class() {
        if ('_init' in this)
            this._init.apply(this, arguments);
    }
    Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
    Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
    return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};

...

Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
    this.x= x;
    this.y= y;
};

Point= Shape.makeSubclass();

Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
    Shape.prototype._init.call(this, x, y);
    this.r= r;
};

… 약간 더 어색한 구문이기는하지만 다른 언어와 비슷하게 보이기 시작합니다. 원하는 경우 몇 가지 추가 기능을 뿌릴 수 있습니다. 어쩌면 makeSubclass클래스 이름을 기억하고 toString사용하여 기본값 을 제공하고 싶을 수도 있습니다 . 어쩌면 new연산자 없이 실수로 호출되었을 때 생성자를 감지하고 싶을 수도 있습니다 (그렇지 않으면 종종 매우 성가신 디버깅이 발생합니다).

Function.prototype.makeSubclass= function() {
    function Class() {
        if (!(this instanceof Class))
            throw('Constructor called without "new"');
        ...

어쩌면 모든 새 멤버를 전달 makeSubclass하고 프로토 타입에 추가하여 Class.prototype...너무 많이 쓰지 않아도 될 수 있습니다 . 다음과 같은 많은 클래스 시스템이이를 수행합니다.

Circle= Shape.makeSubclass({
    _init: function(x, y, z) {
        Shape.prototype._init.call(this, x, y);
        this.r= r;
    },
    ...
});

객체 시스템에서 바람직하다고 생각할 수있는 많은 잠재적 인 기능이 있으며 아무도 특정 공식에 동의하지 않습니다.


폐쇄 방법 , 그럼. 상속을 전혀 사용하지 않음으로써 JavaScript의 프로토 타입 기반 상속 문제를 피할 수 있습니다. 대신 :

function Shape(x, y) {
    var that= this;

    this.x= x;
    this.y= y;

    this.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };
}

function Circle(x, y, r) {
    var that= this;

    Shape.call(this, x, y);
    this.r= r;

    var _baseToString= this.toString;
    this.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+that.r;
    };
};

var mycircle= new Circle();

이제 모든 단일 인스턴스에는 Shape고유 한 toString메소드 사본 (및 추가 한 다른 메소드 또는 다른 클래스 멤버)이 있습니다.

각 클래스 멤버의 자체 사본을 가진 모든 인스턴스의 나쁜 점은 효율성이 떨어진다는 것입니다. 많은 수의 서브 클래스 인스턴스를 처리하는 경우 프로토 타입 상속이 더 적합합니다. 또한 기본 클래스의 메소드 호출은 약간 성가시다. 서브 클래스 생성자가 메소드를 덮어 쓰기 전에 메소드가 무엇인지 기억해야한다.

[또한 여기에는 상속이 없기 때문에 instanceof연산자는 작동하지 않습니다. 필요한 경우 클래스 스니핑을위한 고유 한 메커니즘을 제공해야합니다. 프로토 타입 상속과 비슷한 방식으로 프로토 타입 객체를 피들 링 할 수는 있지만 약간 까다 롭고 실제로 instanceof작업하는 것이 가치가 없습니다 .]

고유 한 메서드를 가진 모든 인스턴스에 대한 좋은 점은 메서드가 해당 메서드를 소유 한 특정 인스턴스에 바인딩 될 수 있다는 것입니다. 이것은 this메소드 호출에서 JavaScript의 이상한 바인딩 방법 때문에 유용합니다 . 메소드에서 소유자와 메소드를 분리하면 결과가 나타납니다.

var ts= mycircle.toString;
alert(ts());

그런 다음 this메서드 내부에 예상대로 Circle 인스턴스가되지 않습니다 (실제로 전역 window객체가되어 광범위한 디버깅 문제가 발생합니다). 방법은 촬영과에 할당 될 때 현실에서 이것은 일반적으로 발생 setTimeout, onclick또는 EventListener일반적이다.

프로토 타입 방식을 사용하면 이러한 모든 과제에 대한 마감을 포함해야합니다.

setTimeout(function() {
    mycircle.move(1, 1);
}, 1000);

또는 앞으로 (또는 지금 Function.prototype을 해킹하는 경우) 다음과 같이 할 수도 있습니다 function.bind().

setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);

인스턴스가 클로저 방식으로 완료되면 바인딩은 인스턴스 변수에 대한 클로저에 의해 무료로 수행됩니다 (일반적으로 that또는 라고 self하지만 개인적으로 후자 self는 JavaScript에서 다른 다른 의미를 가지고 있기 때문에 조언합니다 ). 1, 1위의 스 니펫에서 인수 를 무료로 얻지 못 하므로 다른 클로저가 필요하거나 bind()그렇게 해야하는 경우 여전히 필요 합니다.

클로저 방법에는 많은 변형이 있습니다. 연산자 를 사용하는 대신 this완전히 작성하여 새로 작성하여 that리턴 하는 것을 선호 할 수 있습니다 new.

function Shape(x, y) {
    var that= {};

    that.x= x;
    that.y= y;

    that.toString= function() {
        return 'Shape at '+that.x+', '+that.y;
    };

    return that;
}

function Circle(x, y, r) {
    var that= Shape(x, y);

    that.r= r;

    var _baseToString= that.toString;
    that.toString= function() {
        return 'Circular '+_baseToString(that)+' with radius '+r;
    };

    return that;
};

var mycircle= Circle(); // you can include `new` if you want but it won't do anything

“적절한”방법은 무엇입니까? 양자 모두. 어느 것이 “최고”입니까? 상황에 따라 다릅니다. FOWW 나는 강력한 OO 작업을 할 때 실제 JavaScript 상속을위한 프로토 타입을 만들고 경향이 있으며 간단한 버리기 페이지 효과를 닫습니다.

그러나 두 가지 방법 모두 대부분의 프로그래머에게는 직관적이지 않습니다. 둘 다 잠재적으로 지저분한 변형이 많이 있습니다. 다른 사람의 코드 / 라이브러리를 사용하는 경우 (중간 및 일반적으로 손상된 많은 체계뿐만 아니라) 두 가지를 모두 충족하게됩니다. 일반적으로 인정되는 답변은 없습니다. JavaScript 객체의 놀라운 세계에 오신 것을 환영합니다.

[이것은 JavaScript가 내가 가장 좋아하는 프로그래밍 언어가 아닌 이유 94의 일부였습니다.]


답변

이 패턴을 상당히 자주 사용합니다. 필자가 필요할 때 상당히 많은 유연성을 얻었습니다. 사용시 Java 스타일 클래스와 비슷합니다.

var Foo = function()
{

    var privateStaticMethod = function() {};
    var privateStaticVariable = "foo";

    var constructor = function Foo(foo, bar)
    {
        var privateMethod = function() {};
        this.publicMethod = function() {};
    };

    constructor.publicStaticMethod = function() {};

    return constructor;
}();

생성시 호출되는 익명 함수를 사용하여 새 생성자 함수를 반환합니다. 익명 함수는 한 번만 호출되므로 비공개 정적 변수를 만들 수 있습니다 (클로저 내부에 있으며 클래스의 다른 멤버에게 표시됨). 생성자 함수는 기본적으로 표준 Javascript 객체입니다. 내부에 개인 속성을 정의하면 공용 속성이 this변수에 첨부됩니다 .

기본적으로이 접근 방식은 Crockfordian 접근 방식과 표준 Javascript 객체를 결합하여보다 강력한 클래스를 만듭니다.

다른 Javascript 객체와 마찬가지로 사용할 수 있습니다.

Foo.publicStaticMethod(); //calling a static method
var test = new Foo();     //instantiation
test.publicMethod();      //calling a method


답변

Douglas CrockfordThe Good Parts 에서이 주제에 대해 광범위하게 논의합니다 . 연산자를 사용하여 새 개체를 만들지 않는 것이 좋습니다 . 대신 그는 맞춤형 생성자를 만들 것을 제안합니다. 예를 들어 :

var mammal = function (spec) {
   var that = {};
   that.get_name = function (  ) {
      return spec.name;
   };
   that.says = function (  ) {
      return spec.saying || '';
   };
   return that;
};

var myMammal = mammal({name: 'Herb'});

Javascript에서 함수는 객체이며 new 연산자 와 함께 객체를 구성하는 데 사용할 수 있습니다 . 일반적으로 생성자로 사용되는 함수는 대문자로 시작합니다. 당신은 종종 다음과 같은 것을 본다 :

function Person() {
   this.name = "John";
   return this;
}

var person = new Person();
alert("name: " + person.name);**

새 객체를 인스턴스화하는 동안 new 연산자 를 사용하는 것을 잊어 버린 경우 일반적인 함수 호출 이 이루어 지며 새 객체 대신 전역 객체에 바인딩됩니다.


답변

bobince의 답변 을 계속하려면

es6에서는 이제 실제로 class

이제 할 수 있습니다 :

class Shape {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    toString() {
        return `Shape at ${this.x}, ${this.y}`;
    }
}

따라서 다른 답변과 마찬가지로 원으로 확장하면 다음과 같이 할 수 있습니다.

class Circle extends Shape {
    constructor(x, y, r) {
        super(x, y);
        this.r = r;
    }

    toString() {
        let shapeString = super.toString();
        return `Circular ${shapeString} with radius ${this.r}`;
    }
}

es6에서 약간 더 깔끔하고 읽기가 더 쉽습니다.


다음은 이에 대한 좋은 예입니다.


답변

구조체를 사용하여 이런 식으로 할 수도 있습니다.

function createCounter () {
    var count = 0;

    return {
        increaseBy: function(nb) {
            count += nb;
        },
        reset: function {
            count = 0;
        }
    }
}

그런 다음 :

var counter1 = createCounter();
counter1.increaseBy(4);


답변

또 다른 방법은 http://jsfiddle.net/nnUY4/입니다
(이러한 종류의 객체 생성 및 공개 기능이 특정 패턴을 따르는 지 여부는 알 수 없습니다)

// Build-Reveal

var person={
create:function(_name){ // 'constructor'
                        //  prevents direct instantiation 
                        //  but no inheritance
    return (function() {

        var name=_name||"defaultname";  // private variable

        // [some private functions]

        function getName(){
            return name;
        }

        function setName(_name){
            name=_name;
        }

        return {    // revealed functions
            getName:getName,
            setName:setName
        }
    })();
   }
  }

  // … no (instantiated) person so far …

  var p=person.create(); // name will be set to 'defaultname'
  p.setName("adam");        // and overwritten
  var p2=person.create("eva"); // or provide 'constructor parameters'
  alert(p.getName()+":"+p2.getName()); // alerts "adam:eva"


답변

생성자 호출 중에 “this”를 닫는 트릭을 사용하는 경우, 오브젝트에서 메소드를 호출하지 않으려는 다른 오브젝트에 의해 콜백으로 사용될 수있는 함수를 작성하기위한 것입니다. “범위를 올바르게 만들기”와 관련이 없습니다.

다음은 바닐라 JavaScript 객체입니다.

function MyThing(aParam) {
    var myPrivateVariable = "squizzitch";

    this.someProperty = aParam;
    this.useMeAsACallback = function() {
        console.log("Look, I have access to " + myPrivateVariable + "!");
    }
}

// Every MyThing will get this method for free:
MyThing.prototype.someMethod = function() {
    console.log(this.someProperty);
};

Douglas Crockford 가 JavaScript에 대해 말한 내용 을 읽으면 많은 것을 얻을 수 있습니다 . 존 레식 도 훌륭합니다. 행운을 빕니다!