属性的访问
在JavaScript中,可以通过点(.)或者方括号([])来获取和设置对象属性的值。使用方括号来操作属性,类似于数组的操作,实际上,在JavaScript语言中,每一个对象都可以看成是一个关联数组。
在C、C++和Java等一些强类型语言中,对象只能拥有固定数目的属性,并且这些属性名称必须提前定义好。而JavaScript语言作为一种弱类型语言,程序可以为对象创建任意数量的属性。当使用点运算符访问对象的属性时,属性名是一个标识符,且该标识符必须直接出现在JavaScript程序中,他们不是数据类型,因此无法修改它们。当使用方括号访问属性时,属性名通过字符串来表示,字符串是JavaScript的数据类型,在程序运行时可以修改和创建它们,这种方法为程序的编写提供了更大的灵活性。
访问一个不存在的属性并不会报错,如果在对象o自身的属性或继承的属性中均未找到属性x,属性访问表达式返回undefined。但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错。null和undefined值都没有属性,因此查询这些值的属性会抛出一个类型错误的异常。
属性的继承
JavaScript对象具有“自有属性”,也有一些属性是从原型对象中继承而来的。在查询一个对象o的属性x的时候,首先会查询对象的自有属性,如果不存在,将会继续在o的原型对象中查询属性x,如果原型对象中也没有x,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查询,直到找到x或者查找到一个原型是null的对象为止。可以看到,属性的继承是通过“原型链”来实现的。
在JavaScript中,只有查询属性才能体现继承的存在,而设置属性则和继承无关。例如,我们给对象o的属性x赋值,如果o中已经存在属性x,那么这个赋值操作只改变这个已有属性的值。如果o中不存在属性x,那么赋值操作会给o添加一个新属性x。如果o有一个同名的继承属性x,那么这个继承的属性就会被新创建的同名属性覆盖,而不会去修改原型链。虽然赋值操作和继承无关,但在属性赋值时还是会首先检查原型链,以此判定是否允许赋值操作,如果o继承了一个只读属性x,那么赋值操作是不被允许的。
属性的赋值有三种结果,赋值失败、创建一个新的属性和改变已有属性的值。但是有一个例外,如果属性x是o继承而来的,而这个属性是一个具有setter方法的accessor属性,那么这时将调用setter方法而不是给o创建一个属性x。需要注意的是,setter方法是有对象o调用的,而不是定义这个属性的原型对象调用的,一次如果setter方法定义任意属性,这个操作只针对对象o本身,并不会修改原型链。
属性的删除
delete运算符可以删除对象的属性。它的操作数是一个属性访问表达式。delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性。delete对象不能删除那些可配置性为false的属性。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。
属性的检测
JavaScript对象可以看做属性的集合,我们经常会检测集合中成员的所属关系—判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnProperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也可以做到这一点。
in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性,则返回true。对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false。propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到时自有属性且这个属性的可枚举性为true时它才返回true。通常由JavaScript代码创建的属性都是可枚举的,但某些内置属性是不可枚举的。
除了使用in运算符外,另外一种更简便的方法就是使用“!==”判断一个属性是否是undefined。然而有一种场景只能用in运算符,in可以区分不存在的属性和存在但值为undefined的属性。
属性的枚举
可以使用for/in遍历对象的属性,for/in循环可以遍历对象所有的可枚举的属性,包括自有属性和继承属性。除了for/in循环之外,ES 5还定义了另外两个用以枚举属性名称的函数。第一个就是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。另外一个枚举枚举属性的函数时Object.getOwnPropertyNames(),它和Object.keys()类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
属性getter和setter
对象的属性是有名字、值和一组特性组成。在ES 5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。而getter和setter定义的属性称为存取器属性,一般的对象的属性称为数据属性。
当程序查询存取器属性的值的时候,JavaScript会调用getter方法,这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript调用setter方法,将赋值表达式左侧的值当做参数传入setter。
和数据属性不同,存取器属性不具有可写性。如果属性同时具有getter和setter方法,它就是一个读/写属性,如果属性只有getter方法,它就是一个只读属性,如果它只有setter方法,他就是一个只写属性,读取一个只写属性值将返回undefined。
存取器属性的定义举例如下:
var p = {
x: 1.0,
y: 1.0,
get r(){
return Math.sqrt(this.x*this.x + this.y*this.y);
},
set r(newvalue){
var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
var ratio = newvalue/oldvalue;
this.x *= ratio;
this.y *= ratio;
},
get theta(){return Math.atan2(this.y, this.x);}
};
其中属性r是一个读写属性,theta是一个只读属性。需要注意的是这段代码中this关键字的用法,JavaScript把getter和setter函数当做对象的方法来调用,也就是说在函数体内的this指向当前对象。和数据属性一样,存取器属性也是可以继承的。有很多场景可以使用到存取器属性,比如智能检测属性的写入值以及在每次属性读取时返回不同值,如下代码所示:
var serialnum = { //这个对象产生严格自增的序列号
$n: 0, //$符号暗示这个属性是一个私有属性
get next(){return this.$n++; } //返回当前值,然后自增
set next(n){ //给n设置新的值,当只有当它比当前值大的时候才能设置成功
if(n >= this.$n) this.$n = n;
else throw "序列号的值不能比当前的值小";
}
};
再来一个例子,这个例子通过getter方法来满足不同随机数的生成:var random = {
get octet(){return Math.floor(Math.random()*256);},
get uint16(){return Math.floor(Math.random()*65536);},
get int16(){return Math.floor(Math.random()*65536)-32768;}
};