创建对象
工厂模式
抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。
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函数的区别是:
- 没有显式地创建对象
- 直接将属性和方法赋给了this对象
- 没有return对象
另外,按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
要创建Person的新实例,必须使用 new 操作符,以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 构造函数的prototype属性被用作新对象的原型
- 如果没有显式地返回其他对象,那么就返回此新对象。注意这里是显式返回其他对象,如果return一个基本类型值的话最终还是会返回那个新对象。
最后创建的两个新对象都有一个constructor属性,该属性指向Person。
person1.constructor == Person; //true
person2.constructor == Person; //true
但是还是用instanceof操作符检测对象类型要好一些。这个例子中创建的对象既是Object的实例,也是Person的实例。
person1 instanceof Object; //true
person1 instanceof Person; //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方。
将构造函数当作函数
构造函数与其它函数唯一的区别,就是在与调用它们的方式不同。
- 任何函数,只要通过new操作符来调用,那它就可以作为构造函数;
- 任何函数,如果不通过new操作符来调用,那它跟普通函数也没什么分别。
//当构造函数使用
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"
- 不使用new操作符时,就是在全局作用域中调用一个函数,这时函数内部的this对象总是指向Global对象。因此调用完函数时,属性和方法都被添加给window对象,可以通过window对象来调用sayName方法。
- 也可以使用call或apply在某个特殊对象的作用域中调用Person函数。这里是在对象o的作用域中调用的,因此调用后o就拥有了所有属性和方法。
构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
在之前的例子中,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对象就共享了全局作用域中定义的同一个函数。
这样确实能解决问题,但是在全局作用域中定义的函数只能被某个对象调用,这让全局作用域有些名不副实。另外,如果对象需要定义很多方法,那么就要定义很多个全局函数,那么我们自定义的引用类型就没有封装性而言了。
原型模式
- 我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
- 其实prototype指向的就是通过调用构造函数而创建的那个实例的原型对象。
- 使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
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]]。这个链接存在于实例与原型对象之间,而不是实例与构造函数之间。
isPrototypeOf()
实例指向原型对象的指针[[Prototype]]虽然所有实现中都无法访问到,但可以通过isPrototypeOf()方法来确定是否有这种联系。(原型对象与实例的联系)
Person.prototype.isPrototypeof(person1); //true
Object.getPrototypeOf()
这个方法可以返回[[Prototype]]的值,也就是可以方便地取得一个对象的原型。
Object.getPrototypeOf(person1) == Person.prototype; //true
Object.getPrototypeOf(person1).name; //"nicholas"
搜索属性
当代码读取某个对象的某个属性时,都会执行一次搜索。
- 先找实例本身,如果找到则返回该属性的值
- 若没找到,则继续搜索指针指向的原型对象,若找到则返回该属性的值
这正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。
如果在对象中添加了一个属性,和原型中属性同名:
myObject.foo = "bar";
那么会遵循如下过程:
- 如果原型链上的同名属性是普通数据访问属性,并且没有被标记为只读(writable:false),那么就会在实例对象中添加这个同名属性,并且屏蔽原型上的属性。
这也是最常见的情况。 - 如果原型链上的同名属性被标记为只读(writable:false),那么无法修改已有的属性或者在实例对象上创建屏蔽属性。
- 如果原型链上的同名属性为访问器属性,且是一个setter,那么就会调用这个setter。属性不会被添加到实例对象上,也不会重新定义这个setter。
大多数人都认为在实例上添加同名属性会是第一种情况,但实际上还会有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",来自原型
可见,大多数情况下,
- 当为实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。
- 添加这个属性只会阻止我们访问原型中的那个属性,但是不会修改原型的那个属性。
- 即使又将实例属性设置为null,也不会恢复其指向原型的链接
- 使用delete操作符可以完全删除实例属性,从而又能重新访问原型中的属性。
var person1 = new Person();
var person2 = new Person();
person1.name = "greg";
person1.name; //"greg",来自实例
person2.name; //"nicholas",来自原型
delete person1.name;
person1.name; //"nicholas",来自原型
hasOwnProperty()
- 可以检测一个属性是存在于实例中还是存在于原型中。
- 只在属性存在于实例中时才会返回true
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
原型与in操作符
- 单独使用in操作符:通过对象能够访问到给定属性时返回true,无论该属性存在于实例中还是原型中。
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"
- 返回所有实例属性,不管是否可枚举:Object.getOwnPropertyNames()
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();
- 构造函数中,只在sayName方法不存在时才会将它添加到原型中,而这段代码也只会在初次调用构造函数时才会被执行。并且这里对原型所做的修改可以立即在所有实例中得到反映。
- if语句检查的应该是初始化之后应该存在的任何属性和方法,不必每个都检测,只要检查其中一个就行。
- 对于采用这种模式创建的对象,还可以用instanceof操作符来确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型间的联系。
寄生构造函数模式
基本思想是创建一个函数,该函数的作用是仅仅封装创建对象的代码,然后再返回新创建的对象。
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();
- 其实与工厂模式一模一样,区别只是创建对象时使用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");
colors.toPipedString(); //"red|blue|green"
对于寄生构造函数模式:
- 返回的对象与构造函数或者与构造函数的原型之间没有关系。因此不能依赖instanceof操作符来确定对象类型。
因此少使用这种模式。
稳妥构造函数模式
稳妥对象:没有公共属性,其方法也不引用this的对象。
最适合在一些安全的环境中,或防止数据被其他应用程序改动时使用
与寄生构造函数模式的区别:
- 新创建的实例方法不引用this
- 不使用new操作符调用构造函数
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操作符对这种对象也没有意义。