面向对象的程序设计
我们可以把对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。每个对象都是基于一个引用类型创建的。
理解对象
创建对象最简单的方式:
方式一,创建一个Object的实例
var person = new Object();
person.say = "hello";
person.sayHi = function(){
alert(this.say);
};
方式二,使用对象字面量创建对象
var person = {
say:"hello",
sayHi:function(){
alert(this.say);
}
};
对象属性类型
为了表示特性是内部值,把特性放入两对而方括号中,例如[[Enumerable]]。
ECMAScript中有两种属性:数据属性和访问器属性。
数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值,数据属性有四个描述其行为的特性。
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,默认值为true;
- [[Enumerable]]:表示能否通过for-in循环返回属性,默认值为true;
- [[Writable]]:表示能否修改属性值,默认值为true;
- [[Value]]:包含这个属性额数据值,默认值为undefined。
要修改属性默认的特性,必须使用Object.defineProperty()方法。这个方法接收3个参数:属性所在的对象、属性的名字、一个描述符对象。描述符对象的属性必须是:configurable、enumerable、writable、value,设置其中一个或多个值,可以修改对应的特性值。例如:
var person = {};
Object.defineProperty(person,"name",{
writable:false,
value:"xiaoming"
});
alert(person.name); //"xiaoming";
person.name = "xiaowang";
alert(person.name); //"xiaoming";
在调用Object.defineProperty()方法创建一个新属性时,如果不指定,configurable、enumerable、writable特性的默认值都是false。
访问器属性
访问器属性不包含数据值,他们包含一对儿getter和setter函数,在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下四个特性:
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,默认值是true;
- [[Enumerable]]:表示能否通过for-in循环返回属性,默认值是true;
- [[Get]]:在读取属性时调用的函数,默认值是undefined;
- [[Set]]:在写入属性时调用的函数,默认值是undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。例如:
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(newValue){
if(newValue>2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
定义多个属性
利用Object.defineProperties()方法可以通过描述符一次定义多个属性,这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例如:
var book = {};
Object.defineProperties(book,{
_year:{
writable:true,
value:2004
},
edition:{
writable:true,
value:1
}
});
读取属性的特性
Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符,这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称,返回值是一个对象。例如:
var book = {};
Object.defineProperties(book,{
_year:{
value:2004
},
edition:{
value:1
}
});
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value); //2005
alert(descriptor.configurable); //false
创建对象
虽然Object构造函数或对象字面量都可以用来创建单个变量,但这些方式有个明显的缺点:使用一个接口创建很多对象,会产生大量重复代码,为了解决这个人们开始使用工厂模式的变种。
原型模式
我们创建的每个函数都有一个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
理解原型对象
只要创建了新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。在调用构造函数创建一个新实例后,该实例内部将包含一个指针[[Prototype]]指向构造函数的原型对象。
可以通过isPrototypeOf()方法确定对象之间的关系,例如:
alert(Person.prototype.isPrototypeOf(person1)); //true
也可以通过Object.getPrototypeOf()方法,该方法返回对象的原型,例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
多个对象实例共享原型所保存的属性和方法的基本原理:每当代码读取某个对象的某个属性时,都会执行一次搜索,目标时具有给定名字的属性,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性则返回该值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到这个属性,则返回该属性的值。
使用delete操作符可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是存在于原型中。来自实例返回true,来自原型返回false,例如:
function Person(){
}
Person.prototype.name = "Nicholas";
var person = new Person();
alert(person.hasOwnProperty("name")); //false
person.name = "Greg";
alert(person.hasOwnProperty("name")); //true
原型与in操作符
有两种方式使用in操作符,单独使用和在for-in循环中使用,在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。例如:
function Person(){
}
Person.property.name = "Nicholas";
var person = new Person();
alert("name" in person); //true
在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。
要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
想取得所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
更简单的原型语法
用一个包含所有属性和方法的对象字面量来重写整个原型对象。例如:
function Person(){
}
Person.prototype = {
name:"Nicholas",
age:29,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
};
不过使用这样的方法,本质上会重写默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不在指向Person函数。如果constructor属性很重要,可以像下面这样特意将它设置回适当的值。
Person.prototype = {
constructor:Person
}
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使先创建了实例后修改原型也照样如此。但是如果重写整个原型对象,调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor:Person,
name:"Nicholas",
age:29,
job:"Software Engineer",
sayName:function(){
alert(this.name);
}
};
friend.sayName(); //error
原生对象的原型
所有的原生引用类型(Object、Array、String、等等)都在其构造函数的原型上定义了方法,例如,在Array.prototype中可以找到sort()方法。
alert(typeof Array.prototype.sort); //"function"
原型对象的问题
原型模式最大问题在于其共享的本性导致的,对于包含引用类型值的属性来说,问题就比较突出了。
组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性,这是最广泛、认同度最高的一种创建自定义类型的方法。例如:
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",29,"Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends == person2.friends); //false
alert(person1.sayName == person2.sayName); //true