在JavaScript中,虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码.为了解决这个问题,人们开始使用工厂模式的一种变体.
1.工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程.考虑到在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("Jam",29,"Soft Engineer");
var person2 = createPerson("Sam",24,"Doctor");
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型).随着JavaScript的发展,有一个新模式出现了.
2.构造函数模式
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("Jim",29,"Software Engineer");
var person2 = new Person("Sam",24,"Doctor");
这里,Person()函数取代了createPerson()函数.两者存在不同之处:
1)没有显示的创建对象;
2)直接将属性和方法赋给了this对象;
3)没有return语句;
4)名称首字母大写,借鉴了其他OO语言;
要创建Person的新实例,必须使用new操作符.以这种方式调用构造函数实际上会经历以下4个步骤:
1)创建一个新对象;
2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
3)执行构造函数中的代码(为这个新对象添加属性);
4)返回新对象;
对象的constructor(构造函数)属性最初是用来标识对象类型的.但提到检测对象类型,还是instanceof操作符要更可靠些.我们这个例子中创建的所有对象及时Object的是实例,同时也是Person的实例.
alert(person1.constructor == Person); //true
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方.
1)将构造函数当做函数
构造函数和其他函数的唯一区别,就在于调用它们的方式不同.不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法.任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样.
//当做构造函数使用
var person = new Person("Lucy",23,"QA");
person.sayName();//Lucy
//当做普通函数调用
Person("Linis",23,"QA");//添加到window
window.sayName();//Linis
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,"Tom",25,"Teacher");
o.sayName();//Tom
注:在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象).
2)构造函数的问题
构造函数虽好,但也并非没有缺点,主要问题就是每个方法都要在每个实例上重新创建一遍,ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象.从逻辑角度讲,此时的构造函数也可以这样定义:
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
}
以这种方式创建函数,会导致不同的作用域链和标示符解析,但创建Function新实例的机制仍然是相同的.因此,不同实例上的同名函数是不相等的.
alert(person1.sayName = person2.sayName); //false
然而,创建两个完成同样任务的Function实例没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面.因此,通过把函数定义转移到构造函数外部来解决这个问题.
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
3.原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法.,也就是说,prototype就是通过调用构造函数而创建的那个对象实例的原型对象.使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下所示:
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName();//Tom
var person2 = new Person();
person2.sayName();//Tom
alert(person1.sayName = person2.sayName); //true
1)理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象.在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针.Person.prototype.constructor指向Person.
创建自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的.
2)原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用.在单独使用时,in操作符会在通过对象能够访问给定属性时返回ture,不论该属性存在于实例中还是原型中.
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Jime";
alert(person1.name); //Jime --来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //Tom --来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //Tom --来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true