JavaScript—创建对象的几种模式总结

时间:2021-07-05 20:15:19

最近重温了一遍红皮书,发现这本书中的很多知识点,属于耐看型的,每次都有不一样的收获。下面是对创建对象几种模式的总结。

构造函数或对象字面量都可以用来创建单个对象,但这些方式有很大的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

下面主要介绍创建对象的几种模式:

(一)工厂模式

工厂模式抽象了创建具体对象的过程,考虑到ES5之前无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。

举例如下:

function createPerson(name, age, job){
var o=new Object();
o.name
=name;
o.age
=age;
o.job
=job;
o.sayName
=function(){
alert(
this.name);
};
return o;
}
var person1=createPerson("carry", 22, "teacher");
var person2=createPerson("mary", 25, "Doctor");

可以多次调用createPerson()函数,每次它都会返回一个包含三个属性一个方法的对象。工厂模式解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即知道一个对象的类型)。

(二)构造函数模式

使用构造函数模式将前面的例子重写

function Person(name, age, job){
this.name =name;
this.age=age;
this.job=job;
this.sayName=function(){
alert(
this.name );
};
}
var person1=new Person("carry", 22, "teacher");
var person2=new Person("mary", 25, "Doctor");

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

(1)创建一个新对象

(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

(3)执行这个构造函数中的代码(为这个新对象添加属性)

(4)返回新对象

在前面例子的最后,Person1和Person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。

可以用instanceof操作符来检测对象类型,例如

alert(person1 instanceof Object); //true

alert(person1 instanceof Person); //true

虽然构造函数模式好用,但也有缺点。其主要问题就是,每个方法都要在每个实例上重新创建一遍。可以把函数定义转移到构造函数外面来解决这个问题。

下面是对前面例子的改进

function Person(name, age, job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=sayName;
}
function sayName(){
alert(
this.name );
}
var person1=new Person("carry", 22, "teacher");
var person2=new Person("mary", 25, "Doctor");

这样解决了两个函数做同一件事情的问题。但是新的问题是:(1)在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名副其实。(2)如果对象需要定义很多方法,就要定义很多全局函数。解决这两个问题,可以用以下的原型模式

(三)原型模式

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

下面用原型模式改写前面的例子

function Person(){
}
Person.prototype.name
="mary";
Person.prototype.age
=22;
Person.prototype.name
="Doctor";
Person.prototype.sayName
=function(){
alert(
this.name);
};
var person1=new Person();
person1.sayName();
//mary

var person2=new Person();
person2.sayName();
//mary

alert(person1.sayName
===person2.sayName); //true

由上面例子可以看出,person1与person2访问的都是同一组属性和同一个sayName()函数。

但是,在前面的例子中每次添加一个属性和方法就要敲一遍Person.prototype,为了减少不必要的输入,可以用对象字面量来重写整个原型对象。如下所示

function Person(){
}
Person.prototye
={
constructor:Person,
name:
"mary",
age:
22,
job:
"Doctor",
sayName:
function(){
alert(
this.name);
}
};

注意:使用这种语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性,不再指向Person函数。如果constructor很重要,可以

 像上面那样特意将它设置回适当的值。

原型模式的问题:由于原型中所有属性都是被很多实例共享的,这种共享对函数和包含基本值的属性没有问题,毕竟通过在实例上天际一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题比较严重。而原型模式的缺点是由其共享本质所导致的。

下面的例子清楚地说明了上述问题

function Person(){
}
Person.prototye
={
constructor:Person,
name:
"mary";
age:
22;
job:
"Doctor";
friends:[
"shelby","court"],
sayName:
function(){
alert(
this.name);
}
};
var person1=new Person();
var person2=new Person();
person1.friends.push(
"van");
alert(person1.friends);
//"shelby,court,van"
alert(person2.friends); //"shelby,court,van"
alert(person1.friends===person2.friends) //true

由于由于原型中所有属性都是被很多实例共享的,修改了person1.friends引用的数组,也会在person2.friendsfriends中反应出来。可是实例一般都是要有自己的全部属性的,因此单独用原型模式创建对象并不常见。

(四)组合使用构造函数模式和原型模式

创建自定义类型最常见的方式,就是合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,同时又共享着对方法的引用,最大限度地节省了内存。

以下是对前面代码的重写

function Person(name, age, job){
this.name=name;
this.age=age;
this.job=job;
this.friends=["shely","court"];
}
Person.prototype
={
constructor:Person,
sayName:
function(){
alert(
this.name);
}
};
var person1=new Person("mary", 22, "Doctor");
var person2=new Person("greg", 25, "teacher");

person1.friends.push(
"van");
alert(peson1.friends);
//"shely,court,van"
alert(person2.friends); //"shely,court"
alert(person1.friends===person2.friends); //false
alert(person1.sayName===person2.sayName); //true

这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则实在原型中定义的。修改person1.friends并不会影响person2.friends,因为他们分别引用不同的数组。这种方式是定义引用类型的一种默认方式。

(五)动态原型模式

动态原型模式把所有信息封装在了构造函数中,而通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

来看下面的例子

function Person(name, age, job){
//属性
this.name=name;
this.age=age;
this.job=job;
//方法
if(typeof this.sayName!="function"){
Person.prototype.sayName
=function(){
alert(
this.name);
};
}
}
var friend=new Person("mary", 22, "doctor");
friend.sayName();

注意:

这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。
这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。
不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。
其中,if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if 语句检查每个属性和每个方法;只要检查其中一个即可。

(六)寄生构造函数模式

寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

用寄生构造函数模式改写上面的例子如下

function Person(name, age, job){
var o=new Object();
o.name
=name;
o.job
=job;
o.sayName
=function(){
alert(
this.name);
};
return o;
}
var friend=new Person("mary", 22, "doctor");
friend.sayName();
//mary

除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式是一样的。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,可以使用这个模式。

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");
alert(colors.toPipedString());
//red|blue|green

注意:返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部创建的对象一样。为此,不能依赖Instanceof操作符来确定对象类型。因此,这种模式不常用。

(七)稳妥构造函数模式

要介绍稳妥构造函数模式,首先要明确一个概念。稳妥对象,所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥构造函数模式遵循与寄生构造函数类似的模式,但有两点不同:(1)新创建对象的实例方法不引用this(2)不使用new操作符调用构造函数。

按照稳妥构造函数的要求,可以将前面的例子重写如下

function Person(name, age, job){
//创建要返回的对象
var o=new Object();
//可以在这里定义私有变量和函数

//添加方法
o.sayName=function(){
alert(name);
};
//返回对象
return o;
}

注意:在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name属性了。可以像下面这样使用稳妥的Person构造函数。

var friend=Person("mary", 22, "doctor");

friend.sayName(); //mary

这样,变量friend中保存的是一个稳妥对象,而除了调用sayName()方法之外,没有任何方式访问其数据成员。稳妥构造函数模式的这种安全性,是他非常适合在某些安全执行环境下使用。

上面是对创建对象几种模式的总结。希望能对大家有所帮助。