工厂模式
- 考虑到由于ECMAScript中无法创建类,所以用函数来封装以特定接口创建对象的细节。例子代码如下
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, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
- 函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的person对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构建函数模式
- ECMAScript中的构造函数可用来创建特定类型的对象。像object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,将前面的例子重写代码如下
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, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
- 和工厂模式的区别:没有显式地创建对象;直接将属性和方法赋给了this对象;没有return语句。
- 要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历一下4个步骤
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
- 对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instanceof操作符要更可靠一些。我们在这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,可以通过instanceof操作符验证
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //false
- 构造函数与其他函数的唯一区别,就在于调用它们的方式不同。只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。构造函数的三种函数调用代码
//当做构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
//当做普通函数调用
Person("Greg", 27, "Doctor"); //adds to window
window.sayName(); //"Greg"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
注:当作为普通函数调用时,属性和方法都被添加给window对象了。当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。因此在调用完函数之后,可以通过window对象来调用sayName()方法。
- 构造函数的问题:就是每个方法都要在每个实例上重新创建一遍。以这种方式创建函数,会导致不同的作用域链和标识符解析。不同实例上的同名函数是不相等的,这的确是不必要的;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。如果采用把函数定义转移到构造函数外面来解决这个问题的话。又产生新的问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是外面这个自定义的引用类型就丝毫没有封装性可严了。这些问题可以通过使用原型模式来解决
原型模式
- prototype(原型)属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。例子代码如下
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();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
此时person1和person2访问的都是同一组属性和同一个sayName()函数。
- 理解原型对象
- 创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象
- 在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
- 通俗讲:当调用一个构造函数创建的实例后,该实例内部有一个指向构造函数的原型对象指针。实例可以通过查询对象属性来调用相应的属性。调用读取某个属性时,都会执行一次搜索,搜索先在实例本身中查询,没有查询到则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
- 原型所保存的属性和方法可以被多个对象实例所共享。
- 虽然可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。如果在实例中添加一个和原型中相同的属性,则调用该属性时调用的是实例中的,屏蔽原型中的。例子代码如下
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();
person1.name = "Greg";
alert(person1.name); //"Greg" – from instance
alert(person2.name); //"Nicholas" – from prototype
```
注:可以使用delete操作符,可以完全删除实例属性,从而让我们能过诚信访问原型中的属性。
- 使用**hasOwnProperty()**方法可以检测一个属性是存在于实例中,还是存在于原型中。只有在给定属性存在于对象实例中,才会返回true。
- in操作符会在通过能够访问给定属性时返回true,无论该属性存在于实例还是在原型中。同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
```javascript
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
实例属性的枚举
- Object.keys()方法:可以取得对象上所有可枚举的实例属性。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys); //"name,age,job,sayName"
- Object.getOwnPropertyNames():枚举出所有实例属性,无论它是否可枚举
var keys = Object.getOwnPropertyNames(Person.prototype); alert(keys); //"constructor,name,age,job,sayName"
原型对象问题:省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式最大问题是由其共享本性所导致的。对于包含引用类型值的属性来说,问题就比较突出了。例子如下
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
结果导致了数组的共享,修改一实例的属性值,导致原型的属性值也改变。所以这种情况的发生导致很少有人淡出使用原型模式
组合使用构造函数模式和原型模式
- 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;集两种模式之长。下面代码重写前面的例子。
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,Court,Van" alert(person2.friends); //"Shelby,Court" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。而修改person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为它们分别引用了不同的数组。
这种构造函数与原型换成的模式,可以说是用来定义引用类型的一种默认模式。