>  기사  >  웹 프론트엔드  >  JavaScript(프로토타입)에서 객체 생성 패턴의 진화

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

高洛峰
高洛峰원래의
2016-11-05 13:45:191283검색

객체를 생성하는 모드에는 여러 가지가 있는데, 각 모드의 장단점은 무엇인가요? 가장 완벽한 모델이 있을까요? 아래에서는 다음과 같은 측면에서 객체를 생성하는 여러 모드를 분석합니다.

객체 생성자와 객체 리터럴 방법

공장 모드

사용자 정의 생성자 모드

프로토타입 패턴

사용자 정의 생성자 패턴과 프로토타입 패턴의 조합

동적 프로토타입 패턴, 기생 생성자 패턴, 안전한 생성자 패턴

장 부분: 객체 생성자와 객체 리터럴 메서드

이전에 블로그 게시물 "JavaScript의 객체 리터럴 이해"에서 이 두 가지 방법에 대해 설명했습니다. 익숙하지 않은 경우 클릭하여 한 번 살펴볼 수 있습니다. 장점은 개별 개체를 만드는 데 매우 편리하다는 것입니다. 그러나 이 방법에는 분명한 단점이 있습니다. 동일한 인터페이스를 사용하여 많은 개체를 생성하면 중복된 코드가 많이 생성됩니다. 이 문장을 어떻게 이해해야 할까요? 다음 코드를 살펴보겠습니다.

var person1={
    <strong>name</strong>:"zzw",
    <strong>age</strong>:"21",
    <strong>school</strong>:"xjtu",<br>         <strong> sayName</strong>:<strong>function(){</strong><br><strong>                console.log(this.name);</strong><br><strong>                };</strong>
    }
var person2={
    <strong>name</strong>:"ht",
    <strong>age</strong>:"18",
    <strong>school</strong>:"tjut",<br>         <strong> sayName:function(){</strong><br><strong>            console.log(this.name);</strong><br><strong>          };</strong><br><br>        }

두 개의 유사한 객체를 생성할 때 유사한 객체가 증가함에 따라 age school이라는 이름과 객체 메서드라는 이름을 반복적으로 쓰는 것을 알 수 있습니다. , 코드가 복잡하고 반복적으로 보일 것이 분명합니다. 이 문제를 해결하기 위해 공장 모델이 탄생했습니다.

2부: 팩토리 패턴

앞서 언급한 바가 있습니다. 여러 개체를 생성하고 중복 코드가 많이 생성되는 문제를 해결하기 위해 팩토리 패턴이 탄생했습니다. . 그렇다면 팩토리 패턴은 정확히 무엇입니까? 이 문제를 어떻게 해결합니까? 먼저 공장이란 무엇인지 생각해 볼 수 있다. 내가 아는 한: 공장에서 금형을 제작할 수 있고, 이 금형을 통해 제품을 대량 생산할 수 있으며, 최종적으로는 이를 수정할 수 있습니다(예: 다양한 색상으로 스프레이, 다양한 쉘로 포장). 이렇게 하면 제품을 하나씩 만들 필요가 없어져 효율성이 크게 향상됩니다.

마찬가지로 객체 생성에도 동일한 아이디어가 사용됩니다. 즉, 함수를 통해 객체 생성의 세부 사항을 캡슐화합니다. 마지막으로, 대량의 반복 코드를 생성하는 문제를 해결하기 위해 다양한 매개변수가 이 함수에 직접 전달됩니다. 다음 코드를 살펴보세요.

       function createPerson(name,age,school){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.school=school;
    o.sayName=function(){
        console.log(this.name);
    };
    return o;
}
var person1=createPerson("zzw","21","xjtu");
var person2=createPerson("ht","18","tjut");

여기에 코드가 많은 것 같습니다! 하지만 객체가 2개 더 생성된다면 10개 객체가 더 생성되고 객체가 100개 더 생성되면 어떻게 될까요? 결과는 예측 가능하므로 팩토리 패턴은 단일 객체를 생성하는 객체 생성자 또는 객체 리터럴로 인해 발생하는 대량의 코드 중복 문제를 성공적으로 해결했습니다! 팩토리 패턴은 다음과 같은 특징을 가지고 있습니다.

객체는 함수 내부에서 명시적으로 생성됩니다.

함수 종료 시 새로 생성된 객체를 반환해야 합니다.

그러나 잘 살펴보면 여기서 생성된 person1, person2 등 팩토리 모드로 생성된 객체는 어떤 타입인지 직접적으로 식별할 수 없다는 것을 알 수 있다. 이 문제를 해결하기 위해 사용자 정의 생성자 패턴이 등장했습니다.

3부: 사용자 정의 생성자 패턴

방금 언급했듯이 사용자 정의 생성자 패턴은 객체를 직접 식별할 수 없는 유형의 문제를 해결하는 것으로 보입니다. 따라서 사용자 정의 생성자 패턴은 최소한 두 가지 문제를 해결해야 합니다. 하나: 생성된 객체의 종류를 직접 확인할 수 있습니다. 둘째: 팩토리 패턴이 해결하는 유사한 객체를 다수 생성할 때 발생하는 코드 중복 문제를 해결합니다.

그럼 왜 맞춤 생성자 패턴이라고 할까요? 이는 첫 번째 부분에서 우리가 사용한 Object 생성자가 기본 생성자이기 때문에 분명히 문제를 해결할 수 없기 때문입니다. 사용자 정의 개체 유형의 속성과 메서드를 정의하는 유일한 방법은 사용자 정의 생성자를 만드는 것입니다. 코드는 다음과 같습니다.

function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
    this.sayName=function(){
        console.log(this.name);
    };
}
var person1=new Person("zzw","21","xjtu");
var person2=new Person("ht","18","tjut");

먼저 이 사용자 정의 구성 모드가 첫 번째 문제를 해결하는지 확인합니다. 위 코드 뒤에 다음 코드를 추가합니다.

console.log(person1 <strong>instanceof</strong> Person);//true
console.log(person1 <strong>instanceof</strong> Object);//true

구조는 모두 true가 됩니다. 물론 모든 객체가 Object에서 상속되고 Person의 경우 객체를 생성할 때 문제가 발생하지 않습니다. Person 생성자가 사용되면 person1이 Person 유형이라는 것을 얻는 것은 문제가 되지 않습니다.

두 번째 질문의 답은 뻔합니다. 분명히, 많은 수의 객체를 생성한다고 해서 코드 중복이 발생하지는 않습니다. 따라서 사용자 정의 생성자는 모든 문제를 성공적으로 해결했습니다.

 A 사용자 정의 생성자와 팩토리 패턴의 다음 차이점을 비교해 보겠습니다.

사용자 정의 생성자는 var o = new Object()

와 같은 객체를 명시적으로 생성하지 않습니다. o.name 등을 사용하면 이 개체에 속성과 메서드를 직접 할당하고 이는 결국 새로 생성된 개체를 가리킵니다. (이 개체에 대한 자세한 내용은 내 다른 블로그 게시물 "JavaScript 함수의 아름다움~"에서 볼 수 있습니다.)

객체가 생성되지 않기 때문에 결국 객체가 반환되지 않습니다(참고: 생성자는 값을 반환하지 않으면 기본적으로 새 객체 인스턴스를 반환합니다).

 B 생성자의 경우 다음 사항도 참고해야 합니다.

생성자의 함수 이름은 일반 함수와 구별하기 위해 대문자를 사용해야 합니다.

생성자도 함수이지만 그 기능 중 하나는 객체를 생성하는 것입니다.

构造函数在创建新对象时,必须使用new操作符。

创建的两个对象person1和person2的constructor(构造函数)属性都指向用于创建它们的Person构造函数。

  C 如何理解构造函数也是函数?

  只要证明构造函数也可以像普通函数一样的调用,那么就可以理解构造函数也是函数了。

function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
    this.sayName=function(){
        console.log(this.name);
    };
}
<strong>Person("zzw","21","xjtu");</strong>
sayName();//zzw

  可以看出,我直接使用了Person("zzw","21","xjtu");来像普通函数一样的调用这个构造函数,因为我们把它当作了普通函数,那么函数中的this就不会指向之前所说的对象(这里亦没有对象),而是指向了window。于是,函数一经调用,内部的变量便会放到全局环境中去,同样,对于其中的函数也会在调用之后到全局环境,只是这个内部的函数是函数表达式并未被调用。只有调用即sayName();才能正确输出。

  由此,我们证明了构造函数也是函数。

  D 那么这种自定义构造函数就没有任何问题吗?

  构造函数的问题是在每次创建一个实例时,构造函数的方法都需要再实例上创建一遍。由于在JavaScript中,我们认为所有的函数(方法)都是对象,所以每当创建一个实例对象,都会同时在对象的内部创建一个新的对象(这部分内容同样可以在我的博文《JavaScript函数之美~》中找到)。即我们之前创建的自定义构造函数模式相当于下列代码:

        function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
    this.sayName=new Function("console.log(this.name)");
}
        var person1=new Person("zzw","21","xjtu");
var person2=new Person("ht","18","tjut");

 即我们在创建person1和person2的时候,同时创建了两个sayName为对象指针的对象,我们可以通过下面这个语句做出判断:

console.log(person1.sayName==person2.sayName);//false

      这就证明了如果创建两个对象同时也在每个对象中又各自创建了一个函数对象,但是创建两个完成同样任务的Function实例的确没有必要(况且内部有this对象,只要创建一个对象,this便会指向它)。这就造成了内部方法的重复造成资源浪费。

  E 解决方法。

  如果我们将构造函数内部的方法放到构造函数的外部,那么这个方法便会被person1和person2共享了,于是,在每次创建新对象时就不会同时创建这个方法对象了。如下:

   function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
                this.sayName=sayName;
}
<strong>function sayName(){
    console.log(this.name);
}</strong>
var person1=new Person("zzw","21","xjtu");
var person2=new Person("ht","18","tjut");

                          person1.sayName();//zzw

  应当注意:this.sayName=sayName;中这里等式右边的sayName是一个指针,所以在创建新对象的时候只是创建了一个指向共同对像那个的指针而已,并不会创建一个方法对象。这样便解决了问题。   而外面的sayName函数在最后一句中是被对象调用的,所以其中的this同样是指向了对象。

  

    F新的问题

    如果这个构造函数中需要的方法很多,那么为了保证能够解决E中的问题,我们需要把所有的方法都写在构造函数之外,可是如果这样:

    在全局作用域中定义的函数从未在全局环境中调用,而只会被某个对象调用,这样就让全局作用域有点名不副实。

    如果把所有构造函数中的方法都放在构造函数之外,这样就没有封装性可言了。       

   由此,为了解决F中的问题,接下来不得不提到JavaScript语言中的核心原型模式了。

 

 

第四部分:原型模式

        为什么会出现原型模式呢?这个模式在上面讲了是为了解决自定义构造函数需要将方法放在构造函数之外造成封装性较差的问题。当然它又要解决构造函数能够解决的问题,所以,最终它需要解决以下几个问题。其一:可以直接识别创建的对象的类型。其二:解决工厂模式解决的创建大量相似对象时产生的代码重复的问题。其三:解决构造函数产生的封装性不好的问题。由于这个问题比较复杂,所以我会分为几点循序渐进的做出说明。

A 理解原型对象

   首先,我们应当知道:无论什么时候,只要创建了一个新函数(函数即对象),就会根据一组特定的规则创建一个函数(对象)的prototype属性(理解为指针),这个属性会指向函数的原型对象(原型对象也是一个对象),但是因为我们不能通过这个新函数访问prototype属性,所以写为[[prototype]]。同时,对于创建这个对象的构造函数也将获得一个prototype属性(理解为指针),同时指向它所创建的函数(对象)所指向的原型对象,这个构造函数是可以直接访问prototype属性的,所以我们可以通过访问它将定义对象实例的信息直接添加到原型对象中。这时原型对象拥有一个constructor属性(理解为指针)指向创建这个对象的构造函数(注意:这个constructor指针不会指向除了构造函数之外的函数)。

   你可能会问?所有的函数都是由构造函数创建的吗?答案是肯定的。函数即对象,我在博文《JavaScript函数之美~》中做了详尽介绍。对与函数声明和函数表达式这样建立函数的方法本质上也是由构造函数创建的。

     

   上面的说法可能过于抽象,我们先写出一个例子(这个例子还不是我们最终想要的原型模式,只是为了让大家先理解原型这个概念),再根据代码作出说明:

<strong>        function Person(){}</strong>
        Person.prototype.name="zzw";
        Person.prototype.age=21;
        Person.prototype.school="xjtu";
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
        var person1=new Person();
        var person2=new Person();
        person1.sayName();//zzw
        person2.sayName();//zzw
        console.log(person1.sayName==person2.sayName);//true

  在这个例子中,我们首先创建了一个内容为空的构造函数,因为刚刚讲了我们可以通过访问构造函数的prototype属性来为原型对象中添加属性和方法。于是在下面几行代码中,我们便通过访问构造函数的prototype属性向原型对象中添加了属性和方法。接着,创建了两个对象实例person1和person2,并调用了原型对象中sayName()方法,得到了原型对象中的name值。这说明:构造函数创建的每一个对象和实例都拥有或者说是继承了原型对象的属性和方法。(因为无论是创建的对象实例还是创造函数的prototype属性都是指向原型对象的) 换句话说,原型对象中的属性和方法会被构造函数所创建的对象实例所共享,这也是原型对象的一个好处。

  下面我会画一张图来继续阐述这个问题:

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

从这张图中我们可以看出以下几点:

构造函数和由构造函数创建的对象的prototype指针都指向原型对象。即原型对象既是构造函数的原型对象,又是构造函数创建的对象的原型对象。

原型对象有一个constructor指针指向构造函数,却不会指向构造函数创建的实例。

构造函数的实例的[[prototype]]属性被实例访问来添加或修改原型对象的属性和方法的,而构造函数的prototype属性可以被用来访问以修改原型对象的属性和方法。

person1和person2与他们的构造函数之间没有直接的关系,只是他们的prototype属性同时指向了同一个原型对象而已。 

Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。

虽然这两个实例都不包含属性和方法,但我们却可以调用person1.name,这是通过查找对象属性的过程来实现的。

B.有关于原型对象中的方法以及实例中的属性和原型对象中的属性

为了加深对原型的理解,我在这里先介绍两种方法确定构造函数创建的实例对象与原型对象之间的关系。

  第一种方法:isPrototypeOf()方法,通过原型对象调用,确定原型对象是否是某个实例的原型对象。在之前的代码后面追加下面两句代码:

console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true

  结果不出意外地均为true,也就是说person1实例和person2实例的原型对象都是Person.prototype。

  第二种方法:Object.getPrototypeOf()方法,通过此方法得到某个对象实例的原型。在之前的代码后面追加下面三句代码:

console.log(Object.getPrototypeOf(person1));
console.log(Object.getPrototypeOf(person1)==Person.prototype);<br>          console.log(Object.getPrototypeOf(person1).name);//zzw

 其中第一句代码在控制台中可以直接获得person1的原型对象,如下图所示:

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

      其中第二句代码得到布尔值:true。第三句代码得到了原型对象中的name属性值。

但是,当实例自己本身有和原型中相同的属性名,而属性值不同,在代码获取某个对象的属性时,该从哪里获取呢?

  规则是:在代码读取某个对象而某个属性是,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从实例本身开始,如果在实例中找到了给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象。观察下面的例子。

function Person(){}
Person.prototype.name="zzw";
Person.prototype.age=21;
Person.prototype.school="xjtu";
Person.prototype.sayName=function(){
    console.log(this.name);
};
var person1=new Person();
var person2=new Person();
console.log(person1.name);//zzw
<strong>person1.name="htt";</strong>
<strong>console.log(person1.name);//htt</strong>
console.log(person2.name);//zzw<br>        <strong>  delete</strong> person1.name;<br>         <strong>       console.log(person1.name);//zzw</strong><br>

首先,我们把person1实例的name属性设置为"htt" ,当我们直接获取person1的name属性时,会现在person1本身找该属性(理解为就近原则),找不到,继续向原型对象中寻找。

 当给person1对象添加了自身的属性name时,这次得到的时person1自身的属性,即该属性屏蔽了原型中的同名属性。

 通过倒数第三句代码再次得到了zzw,这说明我们对person1设定了与原型对象相同的属性名,但却没有重写原型对象中的同名属性。

 最后,我们可以通过delete删除实例中的属性,而原型中的属性不会被删除。 

 

  第三种方法:hasOwnProperty()方法

   该方法可以检测一个属性是存在于实例中还是存在于原型中。只有给定属性存在于对象实例中时,才会返回true,否则返回false。举例如下:

       function Person(){}
        Person.prototype.name="zzw";
        Person.prototype.age=21;
        Person.prototype.school="xjtu";
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
        var person1=new Person();
        var person2=new Person();
        console.log(person1.name);//zzw
    <strong>    console.log(person1.hasOwnProperty("name"));//false  因为zzw是搜索于原型对象的</strong>
        person1.name="htt";
        console.log(person1.name);//htt
    <strong>    console.log(person1.hasOwnProperty("name"));//true 在上上一句,我添加了person1实例的属性,它不是属于原型对象的属性</strong>
            delete person1.name;
                console.log(person1.name);//zzw
<strong>                console.log(person1.hasOwnProperty("name"));//false  由于使用delete删除了实例中的name属性,所以为false
</strong>

 C.in操作符的使用以及如何编写函数判断属性存在于对象实例中

  in操作符会在通过对象能够访问给定属性时,返回true,无论该属性存在于事例中还是原型中。观察下面的例子:

        function Person(){}
        Person.prototype.name="zzw";
        Person.prototype.age=21;
        Person.prototype.school="xjtu";
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
        var person1=new Person();
        var person2=new Person();
        console.log(person1.name);//zzw
        console.log(person1.hasOwnProperty("name"));//false
        <strong>console.log("name" in person1);//true</strong>
        person1.name="htt";
        console.log(person1.name);//htt
        console.log(person1.hasOwnProperty("name"));//true
        <strong>console.log("name" in person1);//true</strong>
            delete person1.name;
                console.log(person1.name);//zzw
                console.log(person1.hasOwnProperty("name"));//false
              <strong>  console.log("name" in person1);//true
</strong>

  可以看到,确实,无论属性在实例对象本身还是在实例对象的原型对象都会返回true。

  有了in操作符以及hasOwnProperty()方法我们就可以判断一个属性是否存在于原型对象了(而不是存在于对象实例或者是根本就不存在)。编写hasPrototypeProperty()函数并检验:

                function Person(){}
    <strong>    function hasPrototypeProperty(Object,name){
            return !Object.hasOwnProperty(name)&&(name in Object);
                }</strong>
        Person.prototype.name="zzw";
        Person.prototype.age=21;
        Person.prototype.school="xjtu";
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
        var person1=new Person();
        var person2=new Person();
        console.log(person1.name);//zzw
        <strong>console.log(hasPrototypeProperty(person1,"name"));//true</strong>
        person1.name="htt";
        console.log(person1.name);//htt
<strong>        console.log(hasPrototypeProperty(person1,"name"));//true</strong>
            delete person1.name;
                console.log(person1.name);//zzw
    <strong>    console.log(hasPrototypeProperty(person1,"name"));//true
</strong>

  其中hasPrototypeProperty()函数的判断方式是:in操作符返回true而hasOwnProperty()方法返回false,那么如果最终得到true则说明属性一定存在于原型对象中。(注意:逻辑非运算符!的优先级要远远高于逻辑与&&运算符的优先级)

 

D.for-in循环和Object.keys()方法在原型中的使用

  在通过for-in循环时,它返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。且对于屏蔽了原型中不可枚举的属性(即将[[Enumerable]]标记为false的属性)也会在for-in中循环中返回。(注:IE早期版本中存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中,这里不做详细介绍)

function Person(){}<br>          Person.prototype.name="zzw";
Person.prototype.age=21;
Person.prototype.school="xjtu";
Person.prototype.sayName=function(){
    console.log(this.name);
};
var person1=new Person();
var person2=new Person();
console.log(person1.name);//zzw
person1.name="htt";
console.log(person1.name);//htt
    delete person1.name;
        console.log(person1.name);//zzw
for(var propName in person1){
    console.log(propName);//name age school sayName
}

通过for-in循环,我们可以枚举初name age school sayName这几个属性。由于person1中的[[prototype]]属性不可被访问,因此,我们不能利用for-in循环枚举出它。

  Object.keys()方法接收一个参数,这个参数可以是原型对象,也可以是由构造函数创建的实例对象,返回一个包含所有可枚举属性的字符串数组。如下:

            function Person(){}
        Person.prototype.name="zzw";
        Person.prototype.age=21;
        Person.prototype.school="xjtu";
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
        var person1=new Person();
        var person2=new Person();
        console.log(person1.name);//zzw
        person1.name="htt";
        console.log(person1.name);//htt
        person1.age="18";
    <strong>    console.log(Object.keys(Person.prototype));//["name", "age", "school", "sayName"]
        console.log(Object.keys(person1));//["name", "age"]
        console.log(Object.keys(person2));//[]
</strong>
  

  我们可以从上面的例子中看到,Object.keys()方法返回的是其自身的属性。如原型对象只返回原型对象中的属性,对象实例也只返回对象实例自己创建的属性,而不返回继承自原型对象的实例。

E 更简单的原型语法

  在之前的例子中,我们在构造函数的原型对象中添加属性和方法时,每次都要在前面敲一遍Person.prototype,如果属性多了,这样的方法会显得更为繁琐,那么下面我将介绍给大家一种简单的方法。

  我们知道,原型对象说到底它还是个对象,只要是个对象,我们就可以使用对象字面量方法来创建,方法如下:

        function Person(){}
Person.prototype={
    name:"zzw",
    age:21,
    school:"xjtu",
    sayName:function (){
        console.log(this.name);
    }
};//原来利用Person.prototype.name="zzw"知识对象中的属性,对于对象并没有任何影响,而这里创建了新的对象<br>          

  同样,最开始,我们创建一个空的Person构造函数(大家发现了没有,其实每次我们创建的都是空的构造函数),然后用对象字面量的方法来向原型对象中添加属性。这样既减少了不必要的输入,也从视觉上更好地封装了原型。 但是,这时原型对象的constructor就不会指向Person构造函数而是指向Object构造函数了。

    为什么会这样?我们知道,当我们创建Person构造函数时,就会同时自动创建这个Person构造函数的原型(prototype)对象,这个原型对象也自动获取了一个constructor属性并指向Person构造函数,这个之前的图示中可以清楚地看出来。之前我们使用的较为麻烦的方法(e.g. Person.prototype.name="zzw")只是简单地向原型对象添加属性,并没有其他本质的改变。然而,上述这种封装性较好的方法即使用对象字面量的方法,实际上是使用Object构造函数创建了一个新的原型对象(对象字面量本质即利用Object构造函数创建新对象),注意:此时Person构造函数的原型对象不再是之前的原型对象(而之前的原型对象的constructor属性仍然指向Person构造函数),而和Object构造函数的原型对象一样均为这个新的原型对象。这个原型对象和创建Person构造函数时自动生成的原型对象风马牛不相及。理所应当的是,对象字面量创建的原型对象的constructor属性此时指向了Object构造函数。

      我们可以通过下面几句代码来验证:

        function Person(){}
Person.prototype={
    name:"zzw",
    age:21,
    school:"xjtu",
    sayName:function (){
        console.log(this.name);
    }
};
var person1=new Person();
console.log(Person.prototype.constructor==Person);//false
console.log(Person.prototype.constructor==Object);//true

  通过最后两行代码我们可以看出Person构造函数的原型对象的constructor属性此时不再指向Person构造函数,而是指向了Object构造函数。但是这并被影响我们正常使用,下面几行代码便可以清楚地看出:

    function Person(){}
Person.prototype={
    name:"zzw",
    age:21,
    school:"xjtu",
    sayName:function (){
        console.log(this.name);
    }
};
var person1=new Person();
console.log(person1.name);//zzw
console.log(person1.age);//21
console.log(person1.school);//xjtu
person1.sayName();//zzw

  下面我将以个人的理解用图示表示(如果有问题,请指出):

    第一步:创建一个空的构造函数。function Person(){}。此时构造函数的prototype属性指向原型对象,而原型对象的constructor属性指向Person构造函数。

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

 第二步:利用对象字面量的方法创建一个Person构造函数的新原型对象。

  Person.prototype={
    name:"zzw",
    age:21,
    school:"xjtu",
    sayName:function (){
        console.log(this.name);
    }
};

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

 此时,由于创建了Person构造函数的一个新原型对象,所以Person构造函数的prototype属性不再指向原来的原型对象,而是指向了Object构造函数创建的原型对象(这是对象字面量方法的本质)。但是原来的原型对象的constructor属性仍指向Person构造函数。

   第三步:由Person构造函数创建一个实例对象。

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

这个对象实例的constructor指针同构造它的构造函数一样指向新的原型对象。

  总结:从上面的这个例子可以看出,虽然新创建的实例对象仍可以共享添加在原型对象里面的属性,但是这个新的原型对象却不再指向Person构造函数而指向Object构造函数,如果constructor的值真的非常重要的时候,我们可以像下面的代码这样重新设置会适当的值:

function Person(){}
Person.prototype={
    constructor:Person,
    name:"zzw",
    age:21,
    school:"xjtu",
    sayName:function (){
        console.log(this.name);
    }
};

  这样,constructor指针就指回了Person构造函数。即如下图所示:

JavaScript(프로토타입)에서 객체 생성 패턴의 진화

  值得注意的是:这种方式重设constructor属性会导致它的[[Enumerable]]特性设置位true,而默认情况下,原生的constructor属性是不可枚举的。但是我们可以试用Object.defineProperty()将之修改为不可枚举的(这一部分可以参见我的另一篇博文:《深入理解JavaScript中的属性和特性》)。

F.原生对象的原型

  原型的重要性不仅体现在自定义类型方面,就连所有原生的引用类型,都是使用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。例如在Array.prototype中可以找到sort()方法,而在String.prototype中就可以找到substring()方法。

console.log(typeof Array.prototype.sort);//function
console.log(typeof String.prototype.substring);//function

于是,实际上我们是可以通过原生对象的原型来修改它。比如:   

String.prototype.output=function (){
    alert("This is a string");
}
var message="zzw";
message.output();

   这是,便在窗口中弹出了“This is a string”。尽管可以这样做,但是我们不推荐在产品化的程序中修改原生对象的原型。这样做有可能导致命名冲突等问题。

G.原型模式存在的问题

  实际上,从上面对原型的讲解来看,原型模式还是有很多问题的,它并没有很好地解决我在第四部分初提出的若干问题:“其一:可以直接识别创建的对象的类型。其二:解决工厂模式解决的创建大量相似对象时产生的代码重复的问题。其三:解决构造函数产生的封装性不好的问题。”其中第一个问题解决的不错,通过构造函数便可以直接看出来类型。第二个问题却解决的不好,因为它省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的默认值,我们只能通过在实例上添加同名属性来屏蔽原型中的属性,这无疑也会造成代码重复的问题。第三个问题,封装性也还说的过去。因此原型模式算是勉强解决了上述问题。

  但是这种方法还由于本身产生了额外的问题。看下面的例子:

       function Person(){}
Person.prototype={
    constructor:Person,
    name:"zzw",
    age:21,
    school:"xjtu",
    friends:["pengnian","zhangqi"],
    sayName:function (){
        console.log(this.name);
    }
};
var person1=new Person();
var person2=new Person();
person1.friends.push("feilong");
console.log(person1.friends);//["pengnian","zhangqi","feilong"]
console.log(person2.friends);//["pengnian","zhangqi","feilong"]

  这里我在新建的原型对象中增加了一个数组,于是这个数组会被后面创建的实例所共享,但是person1.friends.push("feilong");这句代码我的意思是添加为person1的朋友而不是person2的朋友,但是在结果中我们可以看到person2的朋友也有了feilong,这就不是我们所希望的了。这也是对于包含引用类型的属性的最大问题。

  也正是这个问题和刚刚提到的第二个问题(即它省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的默认值,我们只能通过在实例上添加同名属性来屏蔽原型中的属性,这无疑也会造成代码重复的问题),很少有人会单单使用原型模式。

第五部分:组合使用自定义构造函数模式和原型模式

  刚刚我们说到的原型模式存在的两个最大的问题。问题一:由于没有在为构造函数创建对象实例时传递初始化参数,所有的实例在默认情况下获取了相同的默认值。问题二:对于原型对象中包含引用类型的属性,在某一个实例中修改引用类型的值,会牵涉到其他的实例,这不是我们所希望的。而组合使用自定义构造函数模式和原型模式即使构造函数应用于定义实例属性,而原型模式用于定义方法和共享的属性。它能否解决问题呢?下面我们来一探究竟!

function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
    this.friends=["pengnian","zhangqi"];
}
Person.prototype={
    constructor:Person,
    sayName:function(){
        console.log(this.name);
    }
}
var person1=new Person("zzw",21,"xjtu");
var person2=new Person("ht",18,"tjut");
person1.friends.push("feilong");
console.log(person1.friends);//["pengnian", "zhangqi", "feilong"]
console.log(person2.friends);//["pengnian", "zhangqi"]
console.log(person1.sayName==person2.sayName);//true

  OK!我们来看看组合使用构造函数模式和原型模式解决的问题:

解决了Object构造函数和对象字面量方法在创建大量对象时造成的代码重复问题(因为只要在创建对象时向构造函数传递参数即可)。

解决了工厂模式产生的无法识别对象类型的问题(因为这里通过构造函数即可获知对象类型)。

解决了自定义构造函数模式封装性较差的问题(这里全部都被封装)。

解决了原型模式的两个问题:所有实例共享相同的属性以及包含引用类型的数组在实例中修改时会影响原型对象中的数组。

  综上所述,组合使用构造函数模式和原型模式可以说是非常完美了。

第六部分:动态原型模式、寄生构造函数模式、稳妥构造函数模式

  实际上,组合使用构造函数模式和原型模式确实已经非常完美了,这里将要讲的几种模式都是在特定的情况下使用的,所以我认为第六部分相对于第五部分并没有进一步的提高。仅仅是多学习几种模式可以解决更多的问题。

A 动态原型模式

  这里的动态原型模式相对于第五部分的组合使用自定义构造函数模式和原型模式本质上是没有什么差别的,只是因为对于有其他OO(Object Oriented,面向对象)语言经验的开发人员看到这种模式会觉得奇怪,因此我们可以将所有信息都封装在构造函数中。本质上是通过检测某个应该存在的方法是否存在或有效,来决定是否要初始化原型。如下例所示: 

function Person(name,age,school){
    this.name=name;
    this.age=age;
    this.school=school;
    if(typeof this.sayName != "function"){
        Person.prototype.sayName=function(){
            console.log(this.name);
        };
    }
}
    var person=new Person("zzw",21,"xjtu");//使用new调用构造函数并创建一个实例对象
    person.sayName(); //zzw
    console.log(person.school);//xjtu

 

  这里先声明了一个构造函数,然后当使用new操作符调用构造函数创建实例对象时进入了构造函数的函数执行环境,开始检测对象的sayName是否存在或是否是一个函数,如果不是,就使用原型修改的方式向原型中添加sayName函数。且由于原型的动态性,这里所做的修改可以在所有实例中立即得到反映。值得注意的是,在使用动态原型模式时,不能使用对象字面量重写原型,否则,在建立了实例的情况下重写原型会导致切断实例和新原型的联系。

B 寄生构造函数模式

  寄生构造函数模式是在前面几种模式都不适用的情况下使用的。看以下例子,再做出说明:

function Person(name,age,school){
    var o =new Object();
    o.name=name;
    o.age=age;
    o.school=school;
    o.sayName=function(){
        console.log(this.name);
    };
    return o;
}
var person = new Person("zzw",21,"xjtu");
person.sayName();//zzw

  寄生构造函数的特点如下:

声明一个构造函数,在构造函数内部创建对象,最后返回该对象,因此这个函数的作用仅仅是封装创建对象的代码。

可以看出,这种方式除了在创建对象的时候使用了构造函数的模式(函数名大写,用new关键字调用)以外与工厂模式一模一样。

构造函数在不返回值的情况下,默认会返回新对象实例,而通过构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

  这个模式可以在特殊的情况下来为对象创建构造函数。假设我们想要创建一个具有额外方法的特殊数组,通过改变Array构造函数的原型对象是可以实现的,但是我在第四部分F中提到过,这种方式可能会导致后续的命名冲突等一系列问题,我们是不推荐的。而寄生构造函数就能很好的解决这一问题。如下所示:

        function SpecialArray(){
    var values=new Array();
    values.push.apply(values,arguments);
    values.toPipedString=function(){
        return this.join("|");
    };
    return values;
}
var colors=new SpecialArray("red","blue","green");
console.log(colors.toPipedString());//red|blue|green

  或者如下所示:

function SpecialArray(string1,string2,string3){
    var values=new Array();
    values.push.call(values,string1,string2,string3);
    values.toPipedString=function(){
        return this.join("|");
    };
    return values;
}
var colors=new SpecialArray("red","blue","green");
console.log(colors.toPipedString());//red|blue|green

  这两个例子实际上是一样的,唯一差别在于call()方法和apply()方法的应用不同。(这部分内容详见《JavaScript函数之美~》)

  这样就既没有改变Array构造函数的原型对象,又完成了添加Array方法的目的。

  关于寄生构造函数模式,需要说明的是:返回的对象与构造函数或构造函数的原型属性之间没有任何关系;也就是说,构造函数返回的对象在与构造函数外部创建的对象没有什么不同。故不能依赖instanceof来确定对象类型。于是,我们建议在可以使用其他模式创建对象的情况下不使用寄生构造函数模式。

 

C.稳妥构造函数模式

  稳妥对象是指这没有公共属性,而且方法也不引用this的对象。稳妥对象适合在安全的环境中使用,或者在防止数据被其他应用程序改动时使用。举例如下:

function Person(name,age,school){
    var o=new Object();
    o.sayName=function (){
        console.log(name);
    };
    return o;
}
var person=Person("zzw",21,"xjtu");
person.sayName();//zzw

 可以看出来,这种模式和寄生构造函数模式非常相似,只是:

  1.新创建对象的实例方法不用this。

  2.不用new操作符调用构造函数(由函数名的首字母大写可以看出它的确是一个构造函数)。

  注意:变量person中保存的是一个稳妥对象,除了调用sayName()方法外没有别的方式可以访问其数据成员。例如在上述代码下添加:

console.log(person.name);//undefined
console.log(person.age);//uncefined

 因此,稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境提供的环境下使用。

 

第七部分:总结

  在这篇博文中,在创建大量相似对象的前提下,我以分析各种方法利弊的思路下向大家循序渐进地介绍了Object构造函数和对象字面量方法、工厂模式、自定义构造函数模式、原型模式、组合使用自定义构造函数模式和原型模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式这几种模式,其中我认为组合使用自定义构造函数模式和原型模式以及动态原型模式都是非常不错的模式。而对于创建对象数量不多的情况下,对象字面量方法、自定义构造函数模式也都是不错的选择。

  这一部分内容属于JavaScript中的重难点,希望大家多读几遍,相信一定会有很大的收获! 


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.