虽然用Object和对象字面量都可以用来创建对象,但这些方式有个明显的缺点,一个接口创建一个对象,会产生很多重复的代码。
工厂模式
考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下所示
function createObject(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 = createObject("zgx",20,"soft engineer");
var person2 = createObject("lyh",20,"teacher");
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别问题(即怎样知道一个对象的类型)
构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
//this.sayName = new Function("alert(this.name)");
}
var person1 = new Person("zgx",20,"soft engineer");
var person2 = new Person("lyh",20,"teacher");
与工厂模式不同之处在于:
- 没有显示的创建对象
- 直接将属性和方法赋值给了this对象
- 没有return语句
要创建Person的新的实例,必须使用new操作符,以这种方式调用构造函数会经历以下4个步骤
- 创建一个新的对象
- 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
- 执行构造函数的代码(为这个新对象添加属性)
- 返回新对象
构造函数解决了对象的识别问题
alert(person1.constructor == Person); //true
alert(person2.constructor == Person);//true
alert(person1 instanceof Object); //true 所有对象均继承自Object
alert(person2 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true
将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。如果不通过new操作符调用,那它普通函数就没有什么区别
//当做构造函数使用
var person = new Person("zgx",20,"sofrware engineer");
person.sayName(); //"zgx"
//当做普通函数调用,在全局作用域中调用一个函数时,this对象总是指向global,在浏览器中就是window
Person("lyh",20,"teacher");
window.sayName(); //"lyh"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"cxx",20,"engineer");
o.sayName(); //"cxx"
构造函数的问题
使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,上面代码中person1和person2拥有各自的sayName()方法。在ECMAScript中,函数也是对象。
alert(pseron1.sayName == person2.sayName); //fasle
一种不优雅的解决方式是把sayName()函数的定义转移到构造函数之外,而在构造函数之类,将sayName属性设置成等于全局的sayName()函数,但这样就没有封装性可言。好在可以通过原型模式来解决
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
原型模式
每创建一个函数都有prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型的好处是可以让所有对象实例共享它所包含的属性和方法
function Person(){}
Person.prototype.name="zgx";
Person.prototype.age = 18;
Person.prototype.job = "software engineer"
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //"zgx"
var person2 = new Person();
person2.sayName(); //"zgx"
person1.__proto__.name="lyh" //__proto__并不是所有浏览器都支持
person1.sayName(); //"lyh"
person2.sayName(); //"lyh"
alert(person1.sayName == person2.sayName) //true
理解原型对象
无论什么时候,只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获取一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针,例如上面的例子。Person.prototype.construstor指向Person,而通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例之后,该实例的内部将包含一个指针,指向构造函数的原型对象。ES5中管这个指针叫[[Prototype]],虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox,Safari,和Chrome在每个对象上都支持一个__proto__属性;而在其他实现中,这个属性对脚本则是完全不可见的,不过,要明确的真正重要的一点就是,这个连接存在于实例和构造函数的原型对象之间,而不是存在于实例和构造函数之间
判断实例和原型对象之间的关系
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ES5中又新增了一个新的方法
alert(Object.getPrototypeOf(person1) == Person.prototype) //true
alert(Object.getPrototypeOf(person1).name); //"zgx"
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用person1.sayName()方法时,会先后进行两次搜索。
虽然我们可以通过对象实例来访问原型对象中的值,但却不能通过对象实例来重写原型对象中的值。如果我们在实例中添加一个和原型中同名的属性,则该属性会屏蔽原型中的那个属性
function Person(){}
Person.prototype.name="zgx";
Person.prototype.age = 18;
Person.prototype.job = "software engineer"
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //"zgx"
var person2 = new Person();
alert(person2.name); //"zgx" 来自原型对象
person2.name = "cxx";
alert(person2.name) //"cxx" 来自实例对象
alert(person1.name) //"zgx" 来自原型对象
delete person2.name
alert(person2.name) //"zgx" 来自原型对象
使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是存在于原型中。只有给定的属性存在于实例对象中才会返回true
ES5中的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法
原型与in操作符
有两种使用in操作符的方式:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例还是原型中。
判断一个属性是否在原型中
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object);
}
在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中即包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回。因为根据规定,所有开发人员定义的属性都是可枚举的--只有IE8及更早版本例外。IE早起版本的实现中存在一个bug,即屏蔽不可枚举属性的实例属性不会出现在for-in循环中。
var o = {
toString:function(){
return "My Object";
}
};
for (var prop in o){
if(prop == "toString"){
alert("Found toString"); //IE中不会显示
}
}
当以上代码运行时,应该会显示一个警告框,表明找到了toString()方法。这里的对象o定义了一个名为toString()的方法,该方法屏蔽了原型中(不可枚举)的toString()方法。在IE中,由于其实现认为原型的toString()方法被打上了值为false的[[Enumerable]]标记,因此应该跳过该属性,结果我们就不会看到警告框。该bug会影响默认不可枚举的所有属性和方法,包括hasOwnProperty()、propertyIsEnumberable()、toLocalString()和valueOf()。ES5也将constructor和prototype属性的[[Enumerable]]特性设置为false,但并不是所有的浏览器都照此实现
要取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。例如
function Person(){}
Person.prototype.name = "zgx";
Person.prototype.age = 18;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "lyh";
p1.age = 31;
var p1Keys = Object.keys(p1);
alert(p1Keys); //"name,age"
如果你想要得到所有实例属性,无论它是否可以枚举,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
更简单的原型语法
更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下所示
function Person(){}
Person.prototype = {
name:"zgx",
age:18,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
}
这里完全重写了默认的Person.prototype,所以constructor属性也变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person) //false
alert(friend.constructor == Object) //true
如果constructor的值真的很只要,可以这样:
function Person(){}
Person.prototype = {
constructor:Person,
name:"zgx",
age:18,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
}
注意以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容ES5的js引擎,可以试试Object.defineProperty(),
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的修改都能够立即从实例上反映出来--即使是先创建了实例后修改原型也照例如此。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
}
friend.sayHi(); //"hi"
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数和最初原型之间的联系。please remember:实例中的指针仅仅指向原型,而不指向构造函数。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor:Person,
name:"zgx",
age:18,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
}
friend.sayName(); //error,因为friend中没有sayName
var f = new Person();
f.sayName(); //"zgx"
上面这个例子,我们先创建了一个Person实例,然后又重写了其原型对象。然后在调用friend.sayName()时发生了错误,因为friend指向的原型中不包含以该名字命名的属性。如下所示:
重写原型对象之前
重写原型对象之后
从上面两个图可以看出重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的关系;它们的引用的仍然是最初的原型
原生对象的原型
原型对象的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型都在其构造函数的原型上定义方法
原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型最大的问题。原型最大的问题是由其共享的本性导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟通过在实例上添加一个同名属性,可以隐藏原型中对应的属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。
function Person(){}
Person.prototype = {
constructor:Person,
name:"zgx",
age:18,
job:"Software Engineer",
friends:["lyh","cxx"],
sayName:function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("ht");
alert(person1.friends); //"lyh,cxx,ht"
alert(person2.friends); //"lyh,cxx,ht"
alert(person1.friends == person2.friends); //true
组合使用构造函数模式和原型模式
创建自定义类型最常见的方式就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省内存。另外,这种混成模式还支持想构造函数传递参数。
function Person(name,age,job){
this.name =name;
this.age = age;
this.job = job;
this.friends = ["lyh","cxx"];
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1 = new Person("zgx",18,"Software Engineer");
var person2 = new Person("ht",18,"teacher");
person1.friends.push("cyl");
alert(person1.friends); //"lyh,cxx,cyl"
alert(person2.friends); //"lyh,cxx"
alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true
动态原型模式
动态原型模式把构造函数和原型封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
function Person(name,age,job){
this.name =name;
this.age = age;
this.job = job;
this.friends = ["lyh","cxx"];
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("zgx",18,"Software Engineer");
friend.sayName();
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
寄生构造函数模式
这种模式的基本思想是创建一个基本函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。
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("zgx",18,"Software Engineer");
friend.sayName();
返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么区别。为此,不能依赖instanceof操作符来确定对象类型。
稳妥构造函数模式
稳妥对象指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁止使用this和new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数模式与寄生构造函数类似,但有两点不同:一是新创建对象的实例方法不引用this,二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:
function Person(name,age,job){
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person("zgx",18,"Software Engineer");
friend.sayName();
这样,变量friend中保存的是一个稳妥对象,而除了调用sayName()方法之外,没有别的方式可以访问其他数据成员。即使有其他代码会给这个对象添加方法或者数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。