每种编程语言都有其自己的设计模式。不禁让人疑惑设计模式是用来做什么?有什么用?
简单的说,设计模式是为了让代码更简洁,更优雅,更完美。
同时设计模式也会让软件的性能更好,同时也会让程序员们更轻松。设计模式可谓是编程界的“葵花宝典”或“辟邪剑法”。如果一旦练成,必可在编程界中来去自如,游刃有余。
下面进入正题
(1)工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程(本书后 面还将讨论其他设计模式及其在 JavaScript中的实现)。考虑到在 ECMAScript中无法创建类,开发人员 就发明了一种函数,用函数来封装以特定接口创建对象的细节。
<html> <meta charset="utf-8"> <head> <script> 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("a",20,"java"); var person2 = createPerson("b",20,"c++"); alert(person1.name); alert(person2.name); </script> </head> <body> </body> </html>
工厂模式,和Java的工厂模式差异不大,就是为了批量生产对象,上述函数我只需写一遍,我就能通过调用函数源源不断的进行复用传参。
简单的说工厂模式就是为了提高函数复用率,写一遍的东西我不要写好几遍就可以调用。
函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无 数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建 多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
任何模式都有其局限性,所以诞生了一个构造函数模式。
(2)构造函数模式
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。例如,前面例子中定义 的 Person()函数可以通过下列任何一种方式来调用。
<html> <meta charset="utf-8"> <head> <script> 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 person = new Person("zs",20,"java"); person.sayName(); Person("ls",20,"c++"); window.sayName(); </script> </head> <body> </body> </html>
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍。在前面的例子中,person1 和 person2 都有一个名为 sayName()的方法,但那 两个方法不是同一个 Function 的实例。不要忘了——ECMAScript中的函数是对象,因此每定义一个 函数,也就是实例化了一个对象
(3)原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那 么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是 可以将这些信息直接添加到原型对象中。
a.理解原型模型
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说, Person.prototype. constructor 指向 Person。而通过这个构造函数,我们还可继续为原型对象 添加其他属性和方法。 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则 都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部 属性),指向构造函数的原型对象。ECMA-262第 5版中管这个指针叫[[Prototype]]。虽然在脚本中 没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 __proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就 是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
b. 原型与 in 操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
<html> <meta charset="utf-8"> <head> <script> function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true person1.name = "Greg"; alert(person1.name); //"Greg" ——来自实例 alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true alert(person2.name); //"Nicholas" ——来自原型 alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true delete person1.name; alert(person1.name); //"Nicholas" ——来自原型 alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true </script> </head> <body> </body> </html>
在以上代码执行的整个过程中,name 属性要么是直接在对象上访问到的,要么是通过原型访问到 的。因此,调用"name" in person1 始终都返回 true,无论该属性存在于实例中还是存在于原型中。 同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性。
c.更简单的原型语法
<html> <meta charset="utf-8"> <head> <script> function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } }; </script> </head> <body> </body> </html>
d.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上 反映出来——即使是先创建了实例后修改原型也照样如此。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
以上代码先创建了 Person 的一个实例,并将其保存在 person 中。然后,下一条语句在 Person. prototype 中添加了一个方法 sayHi()。即使 person 实例是在添加新方法之前创建的,但它仍然可 以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用 person.sayHi() 时,首先会在实例中搜索名为 sayHi 的属性,在没找到的情况下,会继续搜索原型。因为实例与原型 之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存 在那里的函数。 尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重 写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向初原型的 [[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与初原型之间的联系。 请记住:实例中的指针仅指向原型,而不指向构造函数。
例如:
function Person(){ }
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
} };
friend.sayName(); //error
看图:
e.原型对象问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在 默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的大问题。 原型模式的大问题是由其共享的本性所导致的。 原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒 也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属 性。然而,对于包含引用类型值的属性来说,问题就比较突出了.
<html> <meta charset="utf-8"> <head> <script> function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", 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 </script> </head> <body> </body> </html>
在此,Person.prototype 对象有一个名为 friends 的属性,该属性包含一个字符串数组。然后, 创建了 Person 的两个实例。接着,修改了 person1.friends 引用的数组,向数组中添加了一个字符 串。由于 friends 数组存在于 Person.prototype 而非 person1 中,所以刚刚提到的修改也会通过 person2.friends(与 person1.friends 指向同一个数组)反映出来。假如我们的初衷就是像这样 在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部 属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
(4)组合使用构造函数模式和原型模式
创建自定义类型的常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。
<html> <meta charset="utf-8"> <head> <script> function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true </script> </head> <body> </body> </html>
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方 法 sayName()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不 会影响到 person2.friends,因为它们分别引用了不同的数组。 这种构造函数与原型混成的模式,是目前在 ECMAScript中使用广泛、认同度高的一种创建自 定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
(5)动态原型模式
有其他 OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原 型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数 中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
示例如下:
<html> <meta charset="utf-8"> <head> <script> 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("Nicholas", 29, "Software Engineer"); friend.sayName(); </script> </head> <body> </body> </html>
注意构造函数代码中//------部分。这里只在 sayName()方法不存在的情况下,才会将它添加到原 型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修 改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可 以说非常完美。其中,if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆 if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使 用 instanceof 操作符确定它的类型。
(6)寄生构造函数模式
通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式 的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数。
<html> <meta charset="utf-8"> <head> <script> function Person(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 friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas" </script> </head> <body> </body> </html>
在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返 回了这个对象。除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实 是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。
而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。 这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊 数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。
示例:
<html> <meta charset="utf-8"> <head> <script> 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" </script> </head> <body> </body> </html>
在这个例子中,我们创建了一个名叫 SpecialArray 的构造函数。在这个函数内部,首先创建了 一个数组,然后 push()方法(用构造函数接收到的所有参数)初始化了数组的值。随后,又给数组实 例添加了一个 toPipedString()方法,该方法返回以竖线分割的数组值。后,将数组以函数值的形 式返回。接着,我们调用了 SpecialArray 构造函数,向其中传入了用于初始化数组的值,此后又调 用了 toPipedString()方法。 关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属 性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此, 不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情 况下,不要使用这种模式。
(7) 稳妥构造函数模式
道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript中的稳妥对象(durable objects)这 个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象适合在 一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的 实例方法不引用 this;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面 的 Person 构造函数重写如下
<html> <meta charset="utf-8"> <head> <script> function Person(name, age, job){ //创建要返回的对象 var o = new Object(); //可以在这里定义私有变量和函数 //添加方法 o.sayName = function(){ alert(name); }; //返回对象 return o; } </script> </head> <body> </body> </html>
注意,在以这种模式创建的对象中,除了使用 sayName()方法之外,没有其他办法访问 name 的值。 可以像下面使用稳妥的 Person 构造函数。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可 以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传 入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环 境——例如,ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的环境—— 下使用。
注意:
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也 没有什么关系,因此 instanceof 操作符对这种对象也没有意义。
js的工厂模式,构造函数模式,原型模式,组合使用构造函数合原型模式,动态原型模式,寄生构造函数模式,稳妥构造函数模式等
大家有没有从中发现,js的设计模式基本围绕着构造和原型这两个主要点。
该篇只是对js的设计模式作出相应的讲解和知识普及,不熟悉该概念的可以参考此文,大家可以将上述例子,自己动手敲着看,哪怕有经验的Java开发或者js,一起敲敲吧,保证有不一样的体会。