使用Object构造函数和对象字面量都可以创建单个对象,缺点:使用一个借口创建很多对象,会产生很多重复性代码。
1.工厂模式
1 | function createPerson(name, age, job){ |
2.构造函数模式
原生构造函数,例如:Number,Boolean, String, Array, Reg, Date等等
按照惯例,构造函数第一个字母应该大写,主要是为了区别其他函数。
1 | function Person(name, age, job){ |
其中,person1和person2中分别都有一个constructor属性,该属性指向Person。1
2
3
4alert(person1.constructor == Person); // true
alert(person2.constructor == Person); // true
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true
检测对象类型,应该使用instanceof,(还有一个检测类型的typeof,其只能检测基本类型,在检测null和object类型时都返回Object);
a.构造函数的缺点
每个方法都要在每个实例上重新创建一遍,原因:在ECMAScript中函数也是对象,因此,每定义一个函数就相当于创建了一个对象。
创建两个相同任务的function实在没有必要。
1
2
3
4
5
6
7
8
9function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
//在ECMAScript中函数也是对象。从这个角度看,每个Person实例都包含一个不同的Function实例
this.sayName = new Function(){
alert(this.name);
}
}
将方法定义成一个指向全局函数的一个指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
/*
将sayName设置成一个全局函数,而且由于sayName中保存的是一个指向全局函数sayName
的一个指针,因此,person1和person2中的sayName共享了在全局作用域下的sayName函数。
*/
this.sayName = sayName;
}
var sayName = function(){
alert(this.name);
}
var person1 = new Person('xiaoMing', 11, 'writter');
var person2 = new Person('xiaoHong', 12, 'painter');
alert(person1.sayName == person2.sayName); // true;
新问题: 定义很多方法就需定义相等的全局函数,丝毫没有起到封装的作用。
3.原型模式
我们创建每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途:包含特定类型(构造函数)的所有实例共享的属性和方法。
a.理解原型对象:
只要创建了一个函数,就会根据特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
在默认情况下,原型对象都会得到一个constructor(构造函数)属性,这个属性包含一个指向prototype所在函数的指针
1
2
3
4
5
6
7
8
9
10function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
Person.prototype.sayName = function(){
alert(this.name);
}
当调用构造函数创建一个新实例以后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
ECMAScript 5管这个属性叫 [ [prototype] ]
。在Firefox、Safari、Chrome上都支持一个属性proto。
不过明确一点,这个连接存在于实例和构造函数的原型对象之间,不存在于实例和构造函数之间。
在所有实例中都无法访问这个[ [prototype] ]
,但是可以用isPrototypeOf
来确定关系。
1
2
3
4/***
因为在person1中含有一个指向原型对象的指针,所以返回true
***/
alert(Person1.prototype.isPrototypeOf(person1)); // true
在ECMAScript 5中新添加一个方法:getPrototypeOf()
方法,其返回值为[ [prototype] ]
支持情况: IE9+、 Firefox 3.5+、 Safari 5+、 Opera 12+和 Chrome。
可以通过实例对象访问原型对象中的属性和方法,但是无法通过实例修改原型对象中的值。
访问属性、方法,当调用实例person1.name时,首先通过查找实例中是否存在属性name,如果不存在则搜寻原型对象,原型对象不存在则返回undefinde;
如果在实例对象中找到属性name,就不会查找原型对象了。
如果在实例对象person1中添加一个原型对象中已经存在的属性或方法时(例如name属性),这个属性或方法会阻止我们去访问或修改原型对象中的同名属性或方法。
通过方法hasOwnProperty()
方法可以检查属性是否存在于实例中。
b.操作符in与原型对象:
有两种方法操作符in: 单独使用in 与 for-in循环使用。
i. 第一种:单独使用操作符in
in会通过对象给定的属性能够访问时返回true, 无论这个属性存在于实例中还是源性对象中。
ii. 第二种: for-in循环使用
在使用for-in循环时,返回的是能够通过对象访问的、可枚举的属性, 无论这个属性存在于实例中还是源性对象中。
1 | function Person(){ |
在EMACScript 5中,Object.keys()
取得原型对象中所有可枚举的实例属性,返回一个字符串数组。
1
2
3
4
5
6
7
8
9function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
Object.keys(person1); //["name", "age", "job", "sayName"];
在EMACScript 5中,得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames()。
1
2var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // ["constructor", "name", "job", "age", "sayName"];
支持这两个方法的浏览器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 12+和 Chrome。
c.更简单的原型语法
前面例子中,每添加一个属性就要敲一遍Person.prototype,没有封装性可言。因此,更好的做法:重写整个原型对象。
1 | function Person(){ |
在上面的代码中,我们本质上完全重写了默认的prototype对象。因此,constructor属性就变成了新对象的constructor属性(即Object构造函数),不再指向Person,也就无法通过constructor来确定对象的类型了。 之前说过:每当创建一个函数, 就会自动创建他的prototype对象,这个对象也会自动获得constructor属性。
如果constructor真的比较重要:
1
2
3
4
5
6
7
8
9
10
11 function Person(){
}
Person.prototype = {
constructor : Person, //添加constructor属性到Person的原型。
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。默认情况下,原型的 constructor 属性是不可枚举的。因此可以使用兼容的EMACScript的Object.defineProperty()。
1 | function Person(){ |
d.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能立即从实例上反映出来——即使是先创建实例后修改原型。
1 | var friend = new Person(); |
重写原型对象: 调用构造函数时会为实例添加一个指向最初原型的[ [prototype] ]指针,而把原型对象重写为另外一个对象就等于切断了构造函数与原型之间的联系,此时实例指针指向(Object)。请记住:实例中的指针仅指向原型,而不是指向构造函数。
1 | function Person(){ |
重写原型对象切断了现有原型和任何之前已经存在的实例之间的联系;它们引用的仍然是最初的原型。
e.原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、 Array、 String,等等)都在其构造函数的原型上定义了方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。
f.原型对象的问题
问题一:省略了为构造函数传递初始化参数这一个环节,会导致所有实例的属性值都相同
问题二:原型中所有属性是被很多实例所共享的,对于基本值得属性来说没关系的,对于包含引用类型的值来说,问题就比较突出了,就会修改原生对象的属性,并且所有实例访问这个属性也会发生变化。
1 | function Person(){ |
4.组合使用构造函数模式和原型模式
构造函数模式用于定义实力属性,而原型模式用于定义方法和共用的属性。这样,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function 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
5.动态原型模式
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。1
2
3
4
5
6
7
8
9
10
11
12function Person(name, age, job){
this.name = name;
this.age = age;
this.jog = job;
//方法,不需要用if检查每个属性和每个方法,只需要用一个if语句检查其中一个属性即可。
if(typeof sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
6.寄生构造函数模式
创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。1
2
3
4
5
6
7
8
9
10
11
12function 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;
}
7.稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup程序)改动时使用。
1 | function Person(name, age, job){ |
变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可以访问其数据成员。