ECMAScript中把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
所有对象都是Object的实例,我们可以通过new Object()创建一个实例,然后为它添加属性,也可以通过对象字字面量创建对象。但这种方式明显有缺点:使用同一个接口创建很多对象,会产生大量的代码。
创建对象的几种方式的比较以及优缺点。
一、工厂模式
1 function createObject(name,age,job){ 2 var obj=new Object(); 3 obj.name=name; 4 obj.age=age; 5 obj.job=job; 6 obj.sayName=function(){ 7 console.log(this.name); 8 }; 9 return obj; 10 } 11 var person=createObject('hong','20','student'); 12 person.sayName();//hong
虽然解决了创建多个相似对象的问题,但是解决对象问题,即无法通过instanceof来判别是那个对象的实例。
二、构造函数模式
1 function person(name,age,job){ 2 this.name=name; 3 this.age=age; 4 this.job=job; 5 this.sayName=function(){ 6 console.log(this.name); 7 }; 8 } 9 var obj=new person('hong',20,'student'); 10 obj.sayName();
使用构造函数创建一个对象,实际上会经历以下四个步骤:
1、创建一个新对象;
2、将构造函数的作用域赋给新对象,因此this指向了这个新对象;
3、执行构造函数中的代码,为这个新对象赋值;
4、返回新对象
一个构造函数的实例中的constructor属性指向这个构造函数。
如上:obj.constructor=person;//true
将构造当成普通的函数:
构造函数与普通函数的区别在于调用方式的不同。构造函数也是函数。如果直接调用,则会在window对象上添加属性。
1 function person(){ 2 this.name='Tom'; 3 this.age=20; 4 } 5 var tom=new person(); 6 console.log(tom.name,tom.age);//Tom,20 7 person(); 8 console.log(window.name,window.age);//Tom,20
用自定义的构造函数意味着将来可以使用它的实例标识为一种特定的类型,即可以使用instanceof来判定,是那个类的实例。
构造函数的问题:
构造函数中的每一个方法都要在每一个实例上重新创建一遍,两个相同的方法并不是同一个Function实例。因为在ECMAScript中函数是对象,因此每定义一个对象就是实例化一个对象。
1 function person(){ 2 this.name='Tom'; 3 this.age=20; 4 this.sayName=function(){ 5 console.log(this.name); 6 } 7 } 8 var per1=new person(); 9 var per2=new person(); 10 console.log(per1.sayName==per2.sayName);//false
创建完成同样任务的两个Function实例,是没有必要的。可以采用下面的方式:
1 function person(){ 2 this.name='Tom'; 3 this.age=20; 4 this.sayName=sayName; 5 } 6 function sayName(){ 7 console.log(this.name); 8 } 9 var per1=new person(); 10 var per2=new person();
虽然这样写法解决了上面的问题,但是这样也会有问题,在全局作用域中定义的函数只能为某个对象调用,如果方法很多就必需定义很多个全局函数,失去封装性。
三、原型模式
每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法(在chrome和ff中,这个属性可能通过实例的__proto__访问得到)。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象(即调用构造函数生成一个实例中第一步生成的那个对象)。使用原型的优点是:可以让所有实例共享它所有包含的属性和方法。
1 function Person(){ 2 // 3 } 4 Person.prototype.name='Nicholas'; 5 Person.prototype.age=23; 6 Person.prototype.job='student'; 7 Person.prototype.sayName=function(){ 8 console.log(this.name); 9 }; 10 var person1=new Person(); 11 var person2=new Person(); 12 person1.name='tom'; 13 console.log(person1.name,person2.name);//tom Nicholas 14 console.log(person1.sayName==person2.sayName);//true
这个例子中,person1的name被一个新值给屏蔽了。访问person1.name属性时,如果实例存在这个属性就不会去搜索原型了(涉及js变量和方法的访问顺序)。
从图中可以看出Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象除了constructor之外,还有后来添加的属性。而Person的每一个实例包含一个属性(上面提到的__proto__),该属性指向了Person.prototype,即和构造函数没有直接关系。虽然没有办法直接访问prototype,但是可以通过isPtototype()方法来确定对象之间是否存在这种关系。
console.log(Person.prototype.isPrototype(person1));//true
在ECMAScript5中有一方法,叫Object.getPrototypeOf(),可以返回原型
console.log(Object.getPrototypeOf(person1)==Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);//Nicholas
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到与给定名字相同的属性时,则返回该属性值。没有找到则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果还有这个属性则会按着原型链去查找这个属性。
当为一个实例添加属性时,这个属性就会屏蔽原型对象中的同名的属性,即添加的属性会阻止我们访问原型中的属性,但不会修改原型中的属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的链接,使用delete可以完全删除实例属性。
1 function Person(){ 2 // 3 } 4 Person.prototype.name='Nicholas'; 5 Person.prototype.age=23; 6 Person.prototype.job='student'; 7 Person.prototype.sayName=function(){ 8 console.log(this.name); 9 }; 10 var person1=new Person(); 11 person1.name='tom'; 12 console.log(person1.name);//tom 13 console.log(person1.hasOwnProperty('name'));//true 14 delete person1.name; 15 console.log(person1.name);//Nicholas 16 console.log(person1.hasOwnProperty('name'));//false
可以使用hasOwnProperty()检测一个属性存在实例中,还是在原型中。存在实例中返回true,存在原型中返回false。如果使用in运算符无论这个属性存在于原型还是实例中都会返回true。综合使用in和hasOwnProperty()便更加进一步的判断。
1 function hasPrototypeProperty(obj,name){ 2 return !obj.hasOwnProperty(name)&&(name in obj); 3 } 4 function Person(){ 5 // 6 } 7 Person.prototype.name="Tom"; 8 var person1=new Person(); 9 var person2=new Person(); 10 person2.name="Hong"; 11 console.log(hasPrototypeProperty(person1,"name"));//true 12 console.log(hasPrototypeProperty(person2,"name"));//false
原型对象的问题:
默认情况下,所有实例都取得相同的属性值。属性值被所有实例所共享,如果一个操作不小心修改了属性值,可能会导致错误。
四、组合使用构造函数和原型模式
构造函数用于定义实例属性,而原型模式用于定义方法和共享的属性。然后每个实例中会自己的一份实例属性的副本,但同时又共享着对方法的引用。
1 function Person(name,age,job){ 2 this.name=name; 3 this.age=age; 4 this.job=job; 5 this.friends=["shelby","court"]; 6 } 7 Person.prototype={ 8 constructor:Person, 9 sayName:function(){ 10 console.log(this.name); 11 } 12 }; 13 var person1=new Person("tom",29,"students"); 14 var person2=new Person("hong",29,"students"); 15 person1.friends.push("Hong"); 16 console.log(person1.friends);//"shelby,court,Hong" 17 console.log(person2.friends);//"shelby,court" 18 console.log(person1.friends===person2.friends);//false 19 console.log(person1.sayName===person2.sayName);//true
这种组合方式是使用比较广泛的方式。
五、动态原型模式
将所有信息都封装在构造函数中,而通过在构造函数中初始化原型,保持了构造函数和原型的优点。
1 function Person(age,name,job){ 2 this.age=age; 3 this.name=name; 4 this.job=job; 5 if(typeof this.sayName!="function"){ 6 Person.prototype.sayName=function(){ 7 console.log(this.name); 8 }; 9 } 10 }
上页面只有在sayName不存在的情况下才会将它添加到原型中。
六、寄生构造函数模式
基本思想:创建一个函数,该函数的作用仅仅是封闭创建对象的代码。除了使用new操作符把函数叫做构造函数之外,与工厂模式没有区别。
1 function Person(age,name,job){ 2 var o=new Object(); 3 o.name=name; 4 o.age=age; 5 o.job=job; 6 o.sayName=function(){ 7 console.log(this.name); 8 }; 9 return o; 10 } 11 var person2=new Person(20,'hong','student');