抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。
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("nicholas",29,"sde");
var person2 = createPerson("greg",27,"dr");
可以无数次的调用这个函数来创建对象,但没有解决对象识别的问题(即怎样知道一个对象的类型)。
可以创建自定义的构造函数,从而定义对象类型的属性和方法。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("nicholas",29,"sde");
var person2 = new Person("greg",27,"dr");
Person函数与createPerson函数的区别是:
另外,按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
要创建Person的新实例,必须使用 new 操作符,以这种方式调用构造函数实际上会经历以下4个步骤:
最后创建的两个新对象都有一个constructor属性,该属性指向Person。
person1.constructor == Person; //true
person2.constructor == Person; //true
但是还是用instanceof操作符检测对象类型要好一些。这个例子中创建的对象既是Object的实例,也是Person的实例。
person1 instanceof Object; //true
person1 instanceof Person; //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方。
构造函数与其它函数唯一的区别,就是在与调用它们的方式不同。
//当构造函数使用
var person1 = new Person("nicholas",29,"sde");
person.sayName(); //"nicholas"
//作为普通函数调用
Person("greg",27,"dr");
window.sayName(); //"greg"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"kristen",25,"nurse");
o.sayName(); //"kristen"
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
在之前的例子中,person1和person2都有一个名为sayName的方法,但那两个方法不是一个Function函数的实例。由于ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。其实就相当于下面这样:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //与之前那种声明函数在逻辑上是等价的
}
}
从这个角度来看,更容易理解每个Person实例都包含一个不同的Function实例。因此,不同实例上的同名函数是不相等的。
person1.sayName == person2.sayName; //false
然而,创建多个完成同样任务的Function实例确实没有必要。可以考虑把函数定义转移到构造函数外部来解决这个问题:
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("nicholas",29,"sde");
var person2 = new Person("greg",27,"dr");
这个例子中,把sayName函数的定义转移到了构造函数外部,而在构造函数内部,将sayName属性设置成等于全局的sayName函数。由于sayName包含的是一个指向函数的指针,因此person1与person2对象就共享了全局作用域中定义的同一个函数。
这样确实能解决问题,但是在全局作用域中定义的函数只能被某个对象调用,这让全局作用域有些名不副实。另外,如果对象需要定义很多方法,那么就要定义很多个全局函数,那么我们自定义的引用类型就没有封装性而言了。
function Person(){
}
Person.prototype.name = "nicholas";
Person.prototype.age = 29;
Person.prototype.job = "sde";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"nicholas"
var person2 = new Person();
person2.sayName(); //"nicholas"
person1.sayName == person2.sayName; //true
由此,我们将所有属性和方法都直接添加到了Person的prototype属性中,所有实例共享这些属性和方法。换句话说。person1和person2 访问的是同一组属性和同一个sayName函数。
只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。
所有原型对象都会有一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
即,Person.prototype.constructor 指向Person
利用构造函数创建一个实例后,该实例内部将包含一个指向构造函数的原型对象的指针[[Prototype]]。这个链接存在于实例与原型对象之间,而不是实例与构造函数之间。
实例指向原型对象的指针[[Prototype]]虽然所有实现中都无法访问到,但可以通过isPrototypeOf()方法来确定是否有这种联系。(原型对象与实例的联系)
Person.prototype.isPrototypeof(person1); //true
这个方法可以返回[[Prototype]]的值,也就是可以方便地取得一个对象的原型。
Object.getPrototypeOf(person1) == Person.prototype; //true
Object.getPrototypeOf(person1).name; //"nicholas"
当代码读取某个对象的某个属性时,都会执行一次搜索。
这正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。
如果在对象中添加了一个属性,和原型中属性同名:
myObject.foo = "bar";
那么会遵循如下过程:
大多数人都认为在实例上添加同名属性会是第一种情况,但实际上还会有2,3两种特殊情况。
function Person(){
}
Person.prototype.name = "nicholas";
Person.prototype.age = 29;
Person.prototype.job = "sde";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "greg";
person1.name; //"greg",来自实例
person2.name; //"nicholas",来自原型
可见,大多数情况下,
var person1 = new Person();
var person2 = new Person();
person1.name = "greg";
person1.name; //"greg",来自实例
person2.name; //"nicholas",来自原型
delete person1.name;
person1.name; //"nicholas",来自原型
var person1 = new Person();
var person2 = new Person();
person1.hasOwnProperty("name"); //false
person1.name = "greg";
person1.name; //"greg",来自实例
person1.hasOwnProperty("name"); //true
delete person1.name;
person1.name; //"nicholas",来自原型
person1.hasOwnProperty("name"); //false
var person1 = new Person();
var person2 = new Person();
person1.hasOwnProperty("name"); //false
"name" in person1; //true
person1.name = "greg";
person1.name; //"greg",来自实例
person1.hasOwnProperty("name"); //true
"name" in person1; //true
这样,无论属性在实例中还是原型中,in操作符都会返回true。
结合使用hasOwnProperty()和in操作符可以判断一个属性在实例中还是原型中:
//在实例中返回false,不在实例中在原型中返回true
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name)&&(name in object);
}
for-in循环:返回所有能通过对象访问的、可枚举的属性。既包括实例中的属性,也包括原型中的属性。
屏蔽了原型中不可枚举属性的实例属性也能被返回。
返回对象上所有可枚举的实例属性。:Object.keys()
var keys = Object.keys(Person.prototype);
keys; //"name,age,job,sayName"
var p1 = new Person();
p1.name = "rob";
p1.age = 31;
var p1keys = Object.keys(p1);
p1keys; //"name,age"
var keys = Object.getOwnPropertyNames(Person.prototype);
keys; //"constructor,name,age,job,sayName"
结果中包含了不可枚举的constructor属性。
常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
function Person(){
}
Person.prototype = {
name:"nicholas",
age:29,
job:"sde",
sayName:function(){
alert(this.name);
}
};
但是此时constructor属性不再指向Person了。因为这里实际上是完全重写了默认的原型对象,因此constructor属性也就成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。
此时,尽管instanceof能返回正确的结果,但constructor已经无法确定对象的类型了。
var friend = new Person();
friend instanceof Object; //true
friend instanceof Person; //true
friend.constructor == Person; //false
friend.constructor == Object; //true
如果constructor属性非常重要,可以人为设置回适当的值:
function Person(){
}
Person.prototype = {
constructor:Person,
name:"nicholas",
age:29,
job:"sde",
sayName:function(){
alert(this.name);
}
};
由于原生的constructor属性是不可枚举的,而此时以这种方式重设的constructor属性是可枚举的,因此也可以用Object.defineProperty()来设置一下:
function Person(){
}
Person.prototype = {
name:"nicholas",
age:29,
job:"sde",
sayName:function(){
alert(this.name);
}
};
Object.defineProperty(Person.prototype,"constructor",{
enumberable:false,
value:Person
})
由于查找属性是从实例到原型的搜索过程,因此对原型对象所做的任何修改都能够立即从实例上反映出来。即使是先创建了实例后修改原型也是如此。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
}
friend.sayHi(); //"hi"
实例与原型间是一种松散连接关系,实例与原型间只是一个指针,因此在实例中没搜索到sayHi属性时会继续搜索原型,那么就会在原型中找到新的sayHi。
如果是重写整个原型对象,那么情况就不一样了。调用构造函数创建新实例时会为实例添加一个指向最初原型的[[Prototype]]指针,当把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor:Person,
name:"nicholas",
age:29,
job:"sde",
sayName:function(){
alert(this.name);
}
};
friend.sayName(); //error
这个例子中,先创建了实例,后重写了原型对象,以下为这个过程:
重写原型之前所定义的实例依旧指向旧原型,而重写了原型之后,构造函数的prototype指向了新原型,此时构造函数和旧原型已经没有任何关系了,而从此之后再创建的实例也都会指向新原型。
所有原生的引用类型,都在其构造函数的原型上定义了方法。
typeof Array.prototype.sort; //"function"
typeof String.prototype.substring; //"function"
也可以为原生对象的原型定义新方法
String.prototype.startsWith = function(text){
return this.indexOf(text) == 0;
}
var msg = "hello world";
msg.startsWith("hello"); //true
但是不推荐修改原生对象的原型,可能与其他已经支持该方法的实现出现命名冲突,也可能会意外地重写原生方法。
function Person(){
}
Person.prototype = {
constructor:Person,
name:"nicholas",
age:29,
job:"sde",
friends:["shelby","court"],
sayName:function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("van");
person1.friends; //"shelby","court","van"
person2.friends; //"shelby","court","van"
person1.friends == person2.friends; //true
由于Array是引用类型的,因此此属性值保存的是对数组的引用,那么person1.friends与person2.friends指向的是同一个数组,修改了其中一个引用的数组值另一个肯定也会改变,这是原型模式比较突出的问题,因此很少单独使用原型模式。
这样,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。因此这种方式最常用
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,"sde");
var person2 = new Person("greg",27,"dr");
person1.friends.push("van");
person1.friends; //"shelby","court","van"
person2.friends; //"shelby","court"
person1.friends == person2.friends; //false;
person1.sayName == person2.sayName; //true
把所有信息都封装在了构造函数中,通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
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,"sde");
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("nicholas",29,"sde");
friend.sayName();
这个模式可以在特殊的情况下用来为对象创建构造函数。比如想创建一个具有额外方法的数组,但不能直接修改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");
colors.toPipedString(); //"red|blue|green"
对于寄生构造函数模式:
稳妥对象:没有公共属性,其方法也不引用this的对象。
最适合在一些安全的环境中,或防止数据被其他应用程序改动时使用
与寄生构造函数模式的区别:
function Person(name,age,job){
var o = new Object();
//可以在这里定义私有变量和函数(不是设为对象o的属性)
//添加public方法
o.sayName = function(){
alert(name);
}
return o;
}
var friend = Person("nicholas",29,"sde");
friend.sayName(); //"nicholas"
注意:在sayName方法中没有使用this。
以这种模式创建的对象,除了使用sayName()方法之外,没有其他方法访问name的值或其他数据成员。即使还能为这个对象添加方法或数据成员,但也不能有方法能访问传入到构造函数中的原始数据。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。