首页 > 技术文章 > JavaScript创建对象的7种模式总结

jim-blogs 2018-04-21 01:38 原文

1、工厂模式

 1 function createPerson(name, age, job){
 2     var o = new Object();
 3     o.name = name;
 4     o.age = age;
 5     o.job = job;
 6     o.sayName = function(){
 7         alert(this.name);
 8     };    
 9     return o;
10 }
11 
12 var person1 = createPerson("Nicholas", 29, "Software Engineer");
13 var person2 = createPerson("Greg", 27, "Doctor");

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2、构造函数模式

 1 function Person(name, age, job){
 2     this.name = name;
 3     this.age = age;
 4     this.job = job;
 5     this.sayName = function(){
 6         alert(this.name);
 7     };    
 8 }
 9 
10 var person1 = new Person("Nicholas", 29, "Software Engineer");
11 var person2 = new Person("Greg", 27, "Doctor");
12 
13 person1.sayName();   //"Nicholas"
14 person2.sayName();   //"Greg"
15 
16 alert(person1 instanceof Object);  //true
17 alert(person1 instanceof Person);  //true
18 alert(person2 instanceof Object);  //true
19 alert(person2 instanceof Person);  //true
20 
21 alert(person1.constructor == Person);  //true
22 alert(person2.constructor == Person);  //true
23 
24 alert(person1.sayName == person2.sayName);  //false        

与工厂模式的不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

缺点:

使用构造函数,每个方法都要在每个实例上重新创建一遍,不同实例上的同名函数是不相等的,导致内存浪费。

3、原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

 1 function Person(){
 2 }
 3 
 4 Person.prototype.name = "Nicholas";
 5 Person.prototype.age = 29;
 6 Person.prototype.job = "Software Engineer";
 7 Person.prototype.sayName = function(){
 8     alert(this.name);
 9 };
10 
11 var person1 = new Person();
12 person1.sayName();   //"Nicholas"
13 
14 var person2 = new Person();
15 person2.sayName();   //"Nicholas"
16 
17 alert(person1.sayName == person2.sayName);  //true
18 
19 alert(Person.prototype.isPrototypeOf(person1));  //true
20 alert(Person.prototype.isPrototypeOf(person2));  //true
21 
22 //only works if Object.getPrototypeOf() is available
23 if (Object.getPrototypeOf){
24     alert(Object.getPrototypeOf(person1) == Person.prototype);  //true
25     alert(Object.getPrototypeOf(person1).name);  //"Nicholas"
26 }

新对象的这些属性和方法是由所有实例共享的,所以person1和person2访问的都是同一组属性和同一个sayName()函数。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

如果我们在实例中添加了一个与实例原型中的一个属性同名的属性,那么该属性将会屏蔽原型中的那个属性。

 1 function Person(){
 2 }
 3 
 4 Person.prototype.name = "Nicholas";
 5 Person.prototype.age = 29;
 6 Person.prototype.job = "Software Engineer";
 7 Person.prototype.sayName = function(){
 8     alert(this.name);
 9 };
10 
11 var person1 = new Person();
12 var person2 = new Person();
13 
14 person1.name = "Greg";
15 alert(person1.name);   //"Greg" – from instance
16 alert(person2.name);   //"Nicholas" – from prototype

(1)更简单的原型语法

 1 function Person(){
 2 }
 3 
 4 Person.prototype = {
 5     name : "Nicholas",
 6     age : 29,
 7     job: "Software Engineer",
 8     sayName : function () {
 9         alert(this.name);
10     }
11 };
12 
13 var friend = new Person();
14 
15 alert(friend instanceof Object);  //true
16 alert(friend instanceof Person);  //true
17 alert(friend.constructor == Person);  //false
18 alert(friend.constructor == Object);  //true

注意:

这种方法下,constructor属性不再指向Person。每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在上面代码使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向object构造函数),不再指向Person函数。

如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

1 Person.prototype = {
2     constructor : Person,
3     name : "Nicholas",
4     age : 29,
5     job: "Software Engineer",
6     sayName : function () {
7         alert(this.name);
8     }
9 };

注意:以这种方式设置constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。因此如果你使用兼容ECMA Script 5的Javascript引擎,可以试一试Object.defineProperty()。

1 //重设构造函数,只适用于ECMAScript 5兼容的浏览器
2 Object.defineProperty(Person.property, "constructor", {
3     enumerable: false,
4     value: Person
5 });

(2)原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。

1 var friend = new Person();
2 
3 Person.prototype.sayHi = function(){
4     alert("hi");
5 };
6 
7 friend.sayHi();   //"hi" (没有问题!)

但如果是重写整个原型对象,那么情况就不一样。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。

 1 function Person(){
 2 }
 3 
 4 var friend = new Person();
 5         
 6 Person.prototype = {
 7     constructor: Person,
 8     name : "Nicholas",
 9     age : 29,
10     job : "Software Engineer",
11     sayName : function () {
12         alert(this.name);
13     }
14 };
15 
16 friend.sayName();   //error

原型模式的缺点是共享性

 1 function Person(){
 2 }
 3 
 4 Person.prototype = {
 5     constructor: Person,
 6     name : "Nicholas",
 7     age : 29,
 8     job : "Software Engineer",
 9     friends : ["Shelby", "Court"],
10     sayName : function () {
11         alert(this.name);
12     }
13 };
14 
15 var person1 = new Person();
16 var person2 = new Person();
17 
18 person1.friends.push("Van");
19 
20 alert(person1.friends);    //"Shelby,Court,Van"
21 alert(person2.friends);    //"Shelby,Court,Van"
22 alert(person1.friends === person2.friends);  //true

4、组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

 1 function Person(name, age, job){
 2     this.name = name;
 3     this.age = age;
 4     this.job = job;
 5     this.friends = ["Shelby", "Court"];
 6 }
 7 
 8 Person.prototype = {
 9     constructor: Person,
10     sayName : function () {
11         alert(this.name);
12     }
13 };
14 
15 var person1 = new Person("Nicholas", 29, "Software Engineer");
16 var person2 = new Person("Greg", 27, "Doctor");
17 
18 person1.friends.push("Van");
19 
20 alert(person1.friends);    //"Shelby,Court,Van"
21 alert(person2.friends);    //"Shelby,Court"
22 alert(person1.friends === person2.friends);  //false
23 alert(person1.sayName === person2.sayName);  //true

这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

5、动态原型模式

 1 function Person(name, age, job){
 2 
 3     //properties
 4     this.name = name;
 5     this.age = age;
 6     this.job = job;
 7     
 8     //methods
 9     if (typeof this.sayName != "function"){
10     
11         Person.prototype.sayName = function(){
12             alert(this.name);
13         };
14         
15     }
16 }
17 
18 var friend = new Person("Nicholas", 29, "Software Engineer");
19 friend.sayName();

加粗代码只会在初次调用构造函数时才会执行。这里对原型所做的修改,能够立即在所有实例中得到反映。

6、寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

 1 function Person(name, age, job){
 2     var o = new Object();
 3     o.name = name;
 4     o.age = age;
 5     o.job = job;
 6     o.sayName = function(){
 7         alert(this.name);
 8     };    
 9     return o;
10 }
11 
12 var friend = new Person("Nicholas", 29, "Software Engineer");
13 friend.sayName();  //"Nicholas"

7、稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:

  • 新创建对象的实例方法不引用this;
  • 不使用new操作符调用构造函数。
 1 function Person(name, age, job){
 2 
 3     //创建要返回的对象
 4     var o = new Object();
 5 
 6     //可以在这里定义私有变量和函数
 7 
 8     //添加方法
 9     o.sayName = function(){
10         alert(name);
11     };
12 
13     //返回对象
14     return o;
15 }

 

推荐阅读