JavaScript에서 객체의 프로토타입 속성은 객체 유형의 프로토타입에 대한 참조를 반환할 수 있습니다. 다소 헷갈리는 설명입니다. 이를 이해하려면 먼저 객체 유형(Type)과 프로토타입(prototype)의 개념을 올바르게 이해해야 합니다.
1 프로토타입이란 무엇입니까
JavaScript에서 객체의 프로토타입 속성은 객체 유형의 프로토타입에 대한 참조를 반환할 수 있습니다. 다소 헷갈리는 설명입니다. 이를 이해하려면 먼저 객체 유형(Type)과 프로토타입(prototype)의 개념을 올바르게 이해해야 합니다.
앞서 객체의 클래스(Class)와 객체 인스턴스(Instance) 사이에 '생성' 관계가 있다고 말씀드렸습니다. 따라서 '클래스'를 객체 특성의 모델링으로, 객체를 클래스로 간주합니다. 특성 또는 클래스(Class)는 객체의 유형(Type)입니다. 예를 들어 이전 예에서 p1과 p2의 유형은 모두 Point입니다. JavaScript에서는 다음과 같이 instanceof 연산자를 통해 확인할 수 있습니다.
p1 instanceof Point
p2 instanceof Point
Point는 p1과 p2의 유일한 유형이 아니므로 Object는 Point보다 더 일반적인 클래스이므로 Object와 Point 사이에 파생된 관계가 있다고 말합니다. 이 관계를 "상속"이라고 한다는 것은 나중에 알 수 있습니다. 이는 객체 간의 일반화된 관계의 특수한 경우이며 객체지향에서는 없어서는 안 될 기본 관계입니다.
객체 지향 분야에서 설명 가능한 추상 관계의 쌍은 인스턴스와 유형만이 아닙니다. JavaScript에서 또 다른 중요한 추상 관계는 유형(Type)과 프로토타입(prototype)입니다. 이 관계는 더 높은 수준의 추상 관계로, 유형과 인스턴스 간의 추상 관계로 3계층 체인을 형성합니다.
실생활에서 우리는 어떤 것이 다른 것을 기반으로 한다고 종종 말합니다. 이 두 가지는 동일한 유형일 수도 있고 다른 유형일 수도 있습니다. "follow the gourd and draw the gourd"라는 관용어에서, 여기서 조롱박은 프로토타입이고, 조롱박은 타입을 표현하기 위해 "ladle.prototype = 특정 조롱박" 또는 "ladle.prototype = 새로운 조롱박"입니다. ()".
프로토타입을 깊이 이해하기 위해서는 디자인 패턴 중 하나인 프로토타입 패턴을 공부하면 됩니다. 이 패턴의 핵심은 프로토타입 인스턴스를 사용하여 생성할 객체의 유형을 지정하고, 이러한 프로토타입을 복사하여 새로운 객체를 생성하는 것입니다. JavaScript 프로토타입은 이 방법과 유사합니다.
프로토타입 패턴에 대한 자세한 내용은 "디자인 패턴"("Design Patterns")을 참조하세요. 이는 이 기사의 범위를 벗어납니다.
유형과 인스턴스 간의 관계와 달리 프로토타입과 유형 간의 관계에서는 유형이 한 번에 하나의 프로토타입만 가질 수 있어야 합니다(그리고 인스턴스는 분명히 한 번에 여러 유형을 가질 수 있습니다). JavaScript의 경우 이 제한에는 두 가지 의미가 있습니다. 첫 번째는 각 특정 JavaScript 유형에 하나의 프로토타입만 있다는 것입니다. 기본적으로 이 프로토타입은 Object 객체입니다(Object 유형이 아니라는 점에 유의하세요!). 두 번째는 이 객체가 속한 유형이 프로토타입 관계를 만족하는 유형 체인이어야 한다는 것입니다. 예를 들어 p1의 유형은 Point와 Object이고 Object 객체는 Point의 프로토타입입니다. ClassA, ClassB, ClassC 및 Object 유형의 객체가 있는 경우 이 네 가지 클래스는 완전한 프로토타입 체인을 형성해야 합니다.
흥미로운 점은 JavaScript가 유형의 프로토타입의 유형을 지정하지 않는다는 것입니다(이것은 또 다른 매우 어색한 진술입니다). 따라서 어떤 유형이든, 일반적으로 일종의 객체가 될 수 있으므로 객체-유형-프로토타입(객체 ) 링 구조 또는 기타 흥미로운 토폴로지 구조를 형성할 수 있습니다. 이러한 구조는 JavaScript에 다양한 용도를 제공하며 그 중 일부는 영리할 뿐만 아니라 아름다움도 가득합니다. 다음 섹션에서는 주로 프로토타입의 사용법을 소개합니다.
2가지 프로토타입 활용팁
프로토타입 활용 스킬을 이해하기 전에 먼저 프로토타입의 특성을 이해해야 합니다. 우선, JavaScript는 각 유형(Type)에 대한 프로토타입 속성을 제공합니다. 이 속성은 객체를 가리키며 이 객체는 이 유형의 "프로토타입"이 됩니다. 이는 이 유형으로 생성된 모든 객체가 프로토타입의 특성을 가짐을 의미합니다. . 또한 JavaScript 객체는 동적이며 프로토타입도 예외는 아닙니다. 프로토타입에 속성을 추가하거나 빼면 이 유형의 프로토타입이 변경됩니다. 예를 들면 다음과 같습니다.
<script> function Point(x,y) { this.x = x; this.y = y; } var p1 = new Point(1,2); var p2 = new Point(3,4); Point.prototype.z = 0; //动态为Point的原型添加了属性 alert(p1.z); alert(p2.z); //同时作用于Point类型创建的所有对象 </script>
객체 유형의 프로토타입에 a라는 속성이 추가되고, 객체 자체에 a라는 동일한 이름의 속성이 있는 경우, 객체의 a 속성에 접근하면 객체 자체의 속성 프로토타입 속성을 "덮지만" 프로토타입 속성은 사라지지 않습니다. 삭제 연산자를 사용하여 객체 자체의 속성 a를 삭제하면 객체의 프로토타입 속성이 다시 표시됩니다. 이 기능을 사용하면 객체의 속성에 대한 기본값을 설정할 수 있습니다. 예를 들면 다음과 같습니다.
<script> function Point(x, y) { if(x) this.x = x; if(y) this.y = y; } Point.prototype.x = 0; Point.prototype.y = 0; var p1 = new Point; var p2 = new Point(1,2); </script>
위의 예에서는 프로토타입을 통해 Point 객체의 기본값(0,0)을 설정하므로 p1의 값은 (0,0)이고, p2의 값은 (1,2)이며, p2.x를 삭제하고 p2.y를 삭제하여 p2의 값을 (0,0)으로 복원할 수 있습니다. 더 흥미로운 예는 다음과 같습니다.
<script> function classA() { this.a = 100; this.b = 200; this.c = 300; this.reset = function() { for(var each in this) { delete this[each]; } } } classA.prototype = new classA(); var a = new classA(); alert(a.a); a.a *= 2; a.b *= 2; a.c *= 2; alert(a.a); alert(a.b); alert(a.c); a.reset(); //调用reset方法将a的值恢复为默认值 alert(a.a); alert(a.b); alert(a.c); </script>
프로토타입을 사용하면 개체 속성에 대해 읽기 전용 getter를 설정하여 덮어쓰는 것을 방지할 수도 있습니다. 예는 다음과 같습니다.
<script> function Point(x, y) { if(x) this.x = x; if(y) this.y = y; } Point.prototype.x = 0; Point.prototype.y = 0; function LineSegment(p1, p2) { //私有成员 var m_firstPoint = p1; var m_lastPoint = p2; var m_width = { valueOf : function(){return Math.abs(p1.x - p2.x)}, toString : function(){return Math.abs(p1.x - p2.x)} } var m_height = { valueOf : function(){return Math.abs(p1.y - p2.y)}, toString : function(){return Math.abs(p1.y - p2.y)} } //getter this.getFirstPoint = function() { return m_firstPoint; } this.getLastPoint = function() { return m_lastPoint; } this.length = { valueOf : function(){return Math.sqrt(m_width*m_width + m_height*m_height)}, toString : function(){return Math.sqrt(m_width*m_width + m_height*m_height)} } } var p1 = new Point; var p2 = new Point(2,3); var line1 = new LineSegment(p1, p2); var lp = line1.getFirstPoint(); lp.x = 100; //不小心改写了lp的值,破坏了lp的原始值而且不可恢复 alert(line1.getFirstPoint().x); alert(line1.length); //就连line1.lenght都发生了改变 </script>
this.getFirstPoint()를 다음과 같이 다시 작성합니다.
this.getFirstPoint = function()
{
function GETTER(){}; GETTER.prototype = m_firstPoint;
return new GETTER();
}
는 이 문제를 방지하고 m_firstPoint 속성의 읽기 전용 특성을 보장할 수 있습니다.
<script> function Point(x, y) { if(x) this.x = x; if(y) this.y = y; } Point.prototype.x = 0; Point.prototype.y = 0; function LineSegment(p1, p2) { //私有成员 var m_firstPoint = p1; var m_lastPoint = p2; var m_width = { valueOf : function(){return Math.abs(p1.x - p2.x)}, toString : function(){return Math.abs(p1.x - p2.x)} } var m_height = { valueOf : function(){return Math.abs(p1.y - p2.y)}, toString : function(){return Math.abs(p1.y - p2.y)} } //getter this.getFirstPoint = function() { function GETTER(){}; GETTER.prototype = m_firstPoint; return new GETTER(); } this.getLastPoint = function() { function GETTER(){}; GETTER.prototype = m_lastPoint; return new GETTER(); } this.length = { valueOf : function(){return Math.sqrt(m_width*m_width + m_height*m_height)}, toString : function(){return Math.sqrt(m_width*m_width + m_height*m_height)} } } var p1 = new Point; var p2 = new Point(2,3); var line1 = new LineSegment(p1, p2); var lp = line1.getFirstPoint(); lp.x = 100; //不小心改写了lp的值,但是没有破坏原始的值 alert(line1.getFirstPoint().x); alert(line1.length); //line1.lenght不发生改变 </script>사실 객체를 유형의 프로토타입으로 설정하는 것은 이 유형을 인스턴스화하여 객체의 읽기 전용 복사본을 만드는 것과 같습니다. 언제든지 복사본을 변경해도 원본에는 영향을 미치지 않습니다. 변경된 속성을 동일한 이름의 복사본 자체 속성으로 덮어쓰지 않는 한 원본 개체에 대한 변경 사항은 복사본에 영향을 미칩니다. 삭제 작업을 사용하여 동일한 이름을 가진 객체 자체 속성을 삭제하면 프로토타입 속성의 가시성을 복원할 수 있습니다. 다음은 또 다른 예입니다.
<script> function Polygon() { var m_points = []; m_points = Array.apply(m_points, arguments); function GETTER(){}; GETTER.prototype = m_points[0]; this.firstPoint = new GETTER(); this.length = { valueOf : function(){return m_points.length}, toString : function(){return m_points.length} } this.add = function(){ m_points.push.apply(m_points, arguments); } this.getPoint = function(idx) { return m_points[idx]; } this.setPoint = function(idx, point) { if(m_points[idx] == null) { m_points[idx] = point; } else { m_points[idx].x = point.x; m_points[idx].y = point.y; } } } var p = new Polygon({x:1, y:2},{x:2, y:4},{x:2, y:6}); alert(p.length); alert(p.firstPoint.x); alert(p.firstPoint.y); p.firstPoint.x = 100; //不小心写了它的值 alert(p.getPoint(0).x); //不会影响到实际的私有成员 delete p.firstPoint.x; //恢复 alert(p.firstPoint.x); p.setPoint(0, {x:3,y:4}); //通过setter改写了实际的私有成员 alert(p.firstPoint.x); //getter的值发生了改变 alert(p.getPoint(0).x); </script>위의 예는 프로토타입을 사용하여 객체의 여러 복사본을 빠르게 생성할 수 있음을 보여줍니다. 일반적으로 프로토타입을 사용하여 다수의 복잡한 객체를 생성하는 것이 프로토타입을 사용하는 것보다 낫습니다. 다른 방법으로 개체를 복사하는 방법이 훨씬 빠릅니다. 하나의 객체를 프로토타입으로 사용하여 많은 수의 새로운 객체를 생성하는 것이 프로토타입 패턴의 핵심이라는 점에 유의하세요.
다음은 예입니다.
<script> var p1 = new Point(1,2); var points = []; var PointPrototype = function(){}; PointPrototype.prototype = p1; for(var i = 0; i < 10000; i++) { points[i] = new PointPrototype(); //由于PointPrototype的构造函数是空函数,因此它的构造要比直接构造//p1副本快得多。 } </script>위에서 언급한 사용 기술 외에도 프로토타입은 고유한 특성으로 인해 다른 용도로 사용되며 가장 널리 사용되는 방법이자 아마도 가장 잘 알려진 용도는 상속을 시뮬레이션하는 것입니다. 이에 대해서는 다음 섹션에서 설명합니다.
3 프로토타입의 본질
위에서 프로토타입의 역할에 대해 언급했습니다. 이제 규칙을 통해 프로토타입의 본질을 밝혀보겠습니다.
프로토타입은 C의 정적 필드처럼 동작합니다. 프로토타입의 속성으로 속성을 추가합니다. 이 속성은 이 유형으로 생성된 모든 인스턴스에서 공유되지만 이 공유는 읽기 전용입니다. 어떤 경우에도 이 속성을 동일한 이름의 자체 속성으로 덮어쓸 수만 있고 변경할 수는 없습니다. 즉, 객체가 특정 속성을 읽을 때 항상 자신의 도메인의 속성 테이블을 먼저 확인하고, 그러한 속성이 있으면 이 속성을 반환하며, 그렇지 않으면 프로토타입 도메인을 읽고 해당 속성을 반환합니다. 프로토타입 도메인. 또한 JavaScript에서는 프로토타입 필드가 모든 유형의 객체를 참조할 수 있도록 허용합니다. 따라서 프로토타입 필드를 읽은 후에도 속성을 찾을 수 없으면 JavaScript는 프로토타입 필드가 가리키는 객체의 프로토타입 필드를 반복적으로 검색합니다. 객체 자체이거나 주기가 발생할 때까지 다음 다이어그램을 사용하여 프로토타입과 객체 인스턴스 간의 관계를 설명할 수 있습니다.
//TODO:
4 프로토타입의 가치와 제한
위의 분석을 통해 프로토타입을 이해하게 되었는데 이를 통해 객체를 프로토타입으로 활용하여 안전하게 수많은 인스턴스를 생성할 수 있다는 것이 프로토타입의 진정한 의미이자 가치입니다. 프로토타입의 이 기능을 사용하면 객체의 상속을 시뮬레이션할 수 있다는 사실을 나중에 살펴보겠지만, 프로토타입이 상속을 시뮬레이션하는 데 사용되지만 프로토타입이 상속의 중요한 가치이기는 하지만 확실히 핵심은 아니라는 점을 알아야 합니다. 즉, JavaScript 따라서 프로토타입 지원은 객체 상속을 구현하는 데만 사용되는 것이 아닙니다. 프로토타입 상속이 없더라도 JavaScript의 프로토타입 메커니즘은 여전히 매우 유용합니다.
프로토타입은 객체를 프로토타입으로 사용하여 유형의 복사본을 만들기 때문에 한계도 큽니다. 우선, 해당 유형의 프로토타입 필드에 있는 값 복사본으로 동작하지 않고 "부작용"을 가져오는 참조 복사본으로 동작합니다. 프로토타입에서 참조 유형의 속성 값을 변경하면(또 다른 다소 어색한 설명:P) 이 유형으로 생성된 모든 인스턴스에 완전히 영향을 미칩니다. 때로는 이것이 정확히 우리에게 필요한 것일 수도 있지만(예: 특정 클래스의 모든 객체의 기본값 변경) 때로는 이것이 우리가 원하지 않는 것일 수도 있습니다(예: 클래스 상속의 경우)는 다음과 같습니다.
<script> function ClassA() { this.a=[]; } function ClassB() { this.b=function(){}; } ClassB.prototype=new ClassA(); var objB1=new ClassB(); var objB2=new ClassB(); objB1.a.push(1,2,3); alert(objB2.a); //所有b的实例中的a成员全都变了!!这并不是这个例子所希望看到的。 </script>