面向对象的程序设计都有一个特点,就是它们都有一个类的概念。而Javascript中没有类的概念,因此它的对象的定义也与其他语言不一样,在Javascript中把对象定义为:无序属性的集合,其属性可以包含基本值,对象和函数。也相当于说对象是一组没有特定顺序的值。
1、理解对象
创建自定义对象的最简单方式就是创建一个Object的实例。然后再为它添加属性和方法:
var person = new Object();
person.name = 'Nicholas';
person.age = 29;
person.job = 'Software Engineer';
person.sayName = function() {
alert(this.name);
}
上面的例子创建了一个person对象并为添加了三个属性和一份方法。现在对象字面量成为创建这种对象的首选模式,上面的例子可以写成这样:
var person = {
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function() {
alert(this.name);
}
}
这个例子中的person对象和前面的是一样的。
1.1、属性类型
Javascript中有两种属性:数据属性和访问器属性
数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值,数据属性有四个描述其行为的特性:
- [[Configurable]]:表示能否通过delete删除属性从而重新定义苏醒,能否修改属性的特性,或者能否把属性修改为访问器属性,前面定义的属性,它们的特性默认值是true。
- [[Enumerable]]:表示能否通过for-in循环返回属性。前面的例子中的特性默认是true。
- [[Writable]]:表示能否修改属性的值,前面例子中的默认为true。
- [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读,写入属性的时候,把新值保存在这个位置,这个特性的默认值为undefined。
前面例子中定义的对象上的属性,它们的[[Configurable]],[[Enumerable]],[[Writable]]都默认true,而[[Value]]属性被设置为指定的值。如:name:'Nocholas'
。要修改属性默认的特性,必须使用Object.defineProperty()方法,这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中描述符必须是以上四个中的一个。
var person = {};
Object.defineProperty(person,'name',{
writable:false,
value:'hehe'
});
这个例子中,person对象的name属性的值是只读的,如果尝试为它指定值,在非严格模式下,赋值操作将被忽略,在严格模式下,就导致抛出错误。
类似的规则也适用于不可配置的属性:
var person = {};
Object.defineProperty(person,'name',{
configurable:false,
value:'hehe'
});
上面例子表示不可从对象中删除属性,而且一旦把属性变成不可配置的就不能再变回来,再调用Object.defineProperty()方法修改除writabel之外的所有特性,都会导致错误。
2、访问器属性
访问器属性不包含数据值:它们包含一对儿getter和setter函数(不是必须的)。在读取访问器属性是会调用getter函数,在写入访问器属性时,会调用setter方法。访问器属性有四个特性:
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性,能否把属性修改为数据属性。
- [[Enumerable]]:表示能否通过for-in循环返回属性。
- [[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);
_year前面的下划线表示是一种常见的记号,表示只能通过对象方法访问的属性。不一定一定要同时制定getter和setter方法。只指定getter方法意味着属性是不能写,尝试写入会被忽略。只指定setter方法表示属性是不能被读,会返回undefined。
2、创建对象
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有一个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
2.1、工厂模式
考虑都Javascript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节:
function createPerson(name,age,job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(name);
};
return 0;
}
var person1 = createPerson('alex',23,'Software Engineer');
var person2 = createPerson('kim',5,'boy');
可以无数次的调用这个函数,每次都会返回一个包含三个属性一个方法的对象。工厂模式解决了创建多个相似对象的问题,但是没有解决对象识别的问题,(即怎么知道一个对象的类型)。
2.2、构造函数模式
在Javascript中,像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('alex',23,'Software Engineer');
var person2 = new Person('kim',5,'boy');
Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同:
- 没有显示地创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
以创建Person的实例,必须使用new操作符。以这种方式调用构造函数实际上会经历一下四个步骤: - 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回对象
在前面的例子中,person1和person2分别保存这person的一个不容的实例。创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方,在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object,以这种方式定义的对象是定义在Global中的。
将构造函数当作函数
构造函数与其他函数的唯一区别就是调用它们的方式不同。任何函数,只要通过new操作符调用,就可以作为构造函数,任何函数,如果不通过new操作符,那它就和普通函数一样。
构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中person1和person2都有sayName()方法,但两个方法不是同一个Function对象的实例。此时的构造函数也可以这样定义:
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sagName = new Function("alert('this.name')");
}
但是创建两个完成同样任务的Function实例的却没有必要;因此,可以通过把函数定义转移到构造函数外面来解决这个为问题:
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
person1和person2对象共享了在全局作用域中定义的同一个sayName()函数。这样解决了两个函数做同一件事情的问题,但是新问题:在全局作用域中定义的函数实际上只能被某一个对象调用,这让全局作用域有点儿名不副实。而更让人无法接收的是:如果对象需要定义很多方法,那就定义很多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性了,这些问题可以通过原型模式来解决。