JavaScript에서 클래스와 인스턴스를 구현하는 데에는 프로토 타입 방식과 클로저 방식의 두 가지 모델이 있습니다. 둘 다 장점과 단점이 있으며 확장 된 변형이 많이 있습니다. 많은 프로그래머와 라이브러리는 언어의 더 추악한 부분을 처리하기 위해 서로 다른 접근 방식과 클래스 처리 유틸리티 기능을 가지고 있습니다.
결과적으로 혼합 회사에서는 메타 클래스가 혼동되어 약간 다르게 동작합니다. 게다가 대부분의 JavaScript 튜토리얼 자료는 끔찍하며 모든 기반을 다루기 위해 어떤 종류의 타협을 제공하여 혼란스러워합니다. (아마도 저자는 혼란 스러울 수 있습니다. JavaScript의 객체 모델은 대부분의 프로그래밍 언어와 매우 다르며, 여러 곳에서 똑바로 잘못 설계되었습니다.)
prototype way로 시작해 봅시다 . 이것은 당신이 얻을 수있는 가장 JavaScript 고유의 것입니다 : 최소한의 오버 헤드 코드가 있으며 instanceof는 이런 종류의 객체의 인스턴스와 작동합니다.
function Shape(x, y) {
this.x= x;
this.y= y;
}
이 생성자 함수 new Shape
의 prototype
조회 에 작성하여 작성된 메소드에 메소드를 추가 할 수 있습니다 .
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
하고 y
될 undefined
및 프로토 타입의에 할당 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의 일부였습니다.]