JavaScript权威指南--对象
要点概述
对象是javascript的基本数据类型。对象是一种复合值。它将很多值(原始值 或者其他对象)聚合在一起。可通过名字访问这些值。对象也可以看做是属性的无序集合,每个属性都有一个名/值。属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。
然而对象不仅仅是字符串到值的映射,除了可以保持的自有的属性,javascript对象还可以从一个称为原型的对象继承属性。对象的方法通常是继承的属性。这种“原型式继承”(protypal inheritance)是javascript的核心特征。
除了字符串、数字、true、false、null、undefined之外,javascript值都是对象。尽管字符串、数字、和布尔值不是对象。但他们的行为和不可变对象非常相似。
属性包含名字和值,属性名可以是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性。值可以是任意javascript值。或者在(ECMAScript5中)可以是一个getter或者setter函数。除了名字和值之外,每个属性还有一些与之相关的值,称为“属性特性”(property attribute)
- 可写:(writable attribute),表明是否可设置该属性的值。
- 可枚举:(enumerable attribute),表明是否可以通过for/in循环返回该属性
- 可配置:(configurable attribute)表明是否可以删除或修改该属性。
在ECMAScript5之前,通过代码给对象创建的所有属性都是可写可枚举的的。在ECMAScript5之后则可以对这些特性加以配置。
除了基本的属性之外,每个对象还有三个相关的对象特性(object attribute):
- 对象的原型(protype)指向另外一个对象,本对象的属性继承它的原型对象。
- 对象的类(class)是一个表示对象类型的字符串
- 对象的扩展标记(exensible flag)指明了(在ECMAScript5中)指明了是否可以向该对象添加新属性。
最后,我们用下面这些属于对三类javascript对象和两类属性做区分
- 内置对象(native object)是由ECMAscript规范定义的对象或类。例如,数组、函数、日期政策表达式都是内置对象
- 宿主对象(host object)是由javascript解释器所嵌入的宿主环境(比如web浏览器)定义的。客户端javascript中表示网页结果的HTMLElement均是宿主对象。宿主环境定义的方法可以当成普通的javascript的函数对象,那么宿主对象也可以当成内置对象。
- 自定义对象(user-defind object)是由运行的javascript代码创建的对象:
- 自有属性(own property)是直接在对象中定义的属性
- 继承属性(inherited property)是在对象的原型对象中定义的属性。
1.创建对象
1.1.对象直接量
创建对象最简单的方式就在是javascript代码中使用对象的直接量。属性名可以是javascript标识符也可以是字符串直接量(包括空字符串)。属性的值可以是任意类型的javascript表达式,表达式的值(可以是原始值也可以是对象值),就是这个属性的值。
var empty ={};//一个空对象 var point ={x:0,y:0}; //两个属性 var point2 ={x:point.x,y:point.y+1}; var book ={ "main title":"javascript", //属性名字里有空格,必须用字符串表示 'sub-title':"the defintive guide", //属性里有连接字符,因此需要使用双引号 "for":"all adiences", //"for是保留字,因此需要双引号。 author:{ //这个属性的值是一个对象 firstName:"dabid", //这里属性的值也是一个对象 surname:"flangan" //这里的属性名都没有引号 } };
在ES5中,保留字可以用做不带符号的属性名,然而对于ES3来说,使用保留字作属性名必须使用引号。最后一个属性后的逗号可以省略,但在IE会报错。
对象的直接量是一个表达式,这个表达式的每次运算都创建并初始化一个新的对象。每次计算对象直接量的时候,也都会计算它的每个属性的值。也就是说,如果在一个重复的调用函数中循环体内使用了对象的直接量。它将创建很多新对象。并且每次创建的对象的属性值也有可能不同。
1.2.通过new创建对象
通过new创建并初始化一个对象,关键字new后跟随一个函数调用。这里的函数称作构造函数(constructor),构造函数用以初始化一个新创建的对象。javascript语言核心中的原始类型都包含内置构造函数。例如:
var o =new Object();创建一个空对象,和{}一样 var a =new Array(); var d =new Date(); var r =new RegExp("js");
1.3.原型
每一个javascript对象(null除外)都和令一个对象相关联。“另一个”对象就是我们熟知的原型,每一个对象都从原型继承属性。
通过对象直接量创建的对象都有同一个原型函数,并可以通过javascript代码Object.prototype获得对原先对象的引用。通过关键字new和构造函数调用创建的对象的原型就是构造函数prototype属性的值。因此,同使用{}创建对象一样,通过new Object()创建的对象也继承Object.prototype.同样,通过new arrary创建的对象原型就是Arrary.prototype,通过new Array()对象创建的对象的原型就是Date.prototype。
没有原型的对象为数不多,object.prototype就是其中之一。它不继承任何属性。其它原型的对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有 一个继承自Object.prototype的原型。例如Date.prototype的属性继承自Object.protype,因此,new Date()创建的date对象的属性同时继承Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的“原型链”(prototype chain)。
1.4.Object.create()
ECMAScript5定义了一个名为Object.create()的方法,它创建一个新的对象,其中第一个参数就是这个对象的原型。Object.create()提供第二个可选参数,用以对对象的属性进行进一步描述。
Object.create()是一个静态函数,而不是提供给某个对象的调用方法。使用它的方法很简单,只需传入所需的原型对象即可。
var o1 = Object.create({x: 1,y: 2}); //o1继承了属性x和y
可以通过传入参数null来创建一个没有原型的新对象,但通过这样方式创建的对象不会继承任何东西,甚至不包括基础方法。比如toString(),也就是说,它不能和“+”运算符一起工作。
var o2 = Object.create(null); //o2不继承任何属性和方法
如果要创建一个普通的空对象,(比如通过{}或new Object()创建的对象),需要传入Object.prototype:
var o3 =Object.create(Object.prototype); //o3和{}和new Object一样
可以通过任意原型创建新对象(换句话说,可以使任意对象可继承),这是一个强大的特性。在ECMACscript3中可以用类型下面的代码模拟属性继承。
//inherit()返回了一个继承自原型对象p属性的新对象 //这里是有ECMAScript5中的Object.create()函数(如果存在的话) //如果不存在Object.create,则使用其他方法 function inherit(p) { if (p == null) throw TypeError(); //p是一个对象,不能是null if (Object.create) //如果Object.create存在 return Object.create(p); //直接使用它 var t = typeof p; //否则进一步检测 if (t !== "object" && t !== "function") throw TypeError; function f() {}; //定义一个空构造函数 f.prototype = p; //将其原型属性设置p return new f(); //将f()创建p的继承对象 }
注意:inherit()并不能完全代替Object.create(),它不能通过传入null原型来创建对象,而且不能接收可选的第二个参数。
inherit()函数的其中一个用途就是防止库函数无意间(非恶意地)修改那行不受你控制的对象。不是将对象直接作为参数传入函数,而是将它的继承对象传入函数。当函数读取继承对象的属性时,实际上读取的是继承来的值。如果给继承对象的属性赋值,则这些属性只会影响到这个继承对象自身。而不是原始对象:
var o = {x: "donot change this balue"}; library_function(inherit(o));//防止对o的意外修改
2.属性的查询和设置
我们可以通过点.和方括号[]运算符来获取属性的值。运算符的左侧应当是一个表达式,它返回一个对象。对于.来说,右侧必须是一个属性名称命名的简单标识符。对于方括号来说[],内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名字:
var Author = book.author; //得到book的“author”属性 var Name = author.surname //得到author的“surname”的属性 var Title =book["main title"] //得到book的main title属性
和查询属性值的写法一样,通过点和方括号也可以创建属性或给属性赋值,但需要将他们放在属性表达式的左侧。
在ECMAScript3中 ,点运算符后的标识符不能是保留字,比如o.for或o.class是非法的,因为for是javascript关键字,class是保留字。如果一个对象的属性名是保留字,则必须使用方括号的形式访问他们。比如o["for"]或o["classs"].ECMAScript5对此放宽了限制。可以在点运算符后直接使用保留字。
当使用方括号时,我们说方括号内的表达式必须返回字符串。其实更严格的讲,表达式必须返回字符串或返回一个科可转换的字符串的值、在第七章有一些例子里的方括号内使用了数字。这样的情况很常见。
2.1.作为关联数组的对象
上文提到,下面的两个表达式的值相同:
object.property object["property"]
第二种语法使用方括号和一个字符串。看起来更像数组,只是这个数组元素是通过字符串索引而不是数字索引。这种数组就是我们所说的关联数组(associative array),也称作散列,映射或字典。javascript对象都是关联数组,本节讨论其重要性。
在c c++和java一些强类型(strong typed)语言中,对象只能拥有固定数目的属性,并且这些属性的名称需要提前定义好。由于javascript是弱类型语言,因此不必遵守这条规定,在任何程序对象中都可以创建任意数量的属性。但当通过.运算符访问对象的属性时,属性名用一个标识符来表示。标识符必须出现在javascript程序中,它们不是数据类型,因此程序无法修改它们。
反过来讲,当通过[]来访问对象的属性时,属性名通过字符串来表示。字符串是javascript的数据类型。在程序运行时都可以修改和创建它们。因此,在javascript可以使用下面的代码:
var addr = ""; for (i = 0; i < 4; i++) { addr += customer["address" + i] + '\n'; };
这段代码读取customer对象的address0 address1 address2 address3 并将它们连接起来。
这个例子主要说明了使用数组写法和用字符串表达式来访问对象属性的灵活性。比如有些场景的属性名是动态生成的话就可以使用这种方式来访问了。
当使用for/in循环遍历关联数组时,就可以体会到for/in强大之处。下面的例子 就是for/in计算portfolio的总计算值:
function getvalue(protfolio) { var total = 0.0; for (stock in protfolio) { //遍历protfolio中的每只股票 var shares = protfolio[stock]; //得到每只股票的份额 var price = getquote(stock); //查找股票的价格 total += shares * price; //将结果累加到total中 } return total; }
2.2.继承
javascript对象具有“自有属性”(own property),也有一些属性是从原型对象继承而来的。
假设要查询对象o的属性x,如果o中不存在x,那么将会继续在o的原型对象中查询属性x。如果原型对象中也没有x,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查找。直到找到x或者查找到一个原型是null为止。可以看到,对象的原型属性构成了一个“链”,通过这个“链”可以实现属性的继承。
//inherit()返回了一个继承自原型对象p属性的新对象 //这里是有ECMAScript5中的Object.create()函数(如果存在的话) //如果不存在Object.create,则使用其他方法 function inherit(p) { if (p == null) throw TypeError(); //p是一个对象,不能是null if (Object.create) //如果Object.create存在 return Object.create(p); //直接使用它 var t = typeof p; //否则进一步检测 if (t !== "object" && t !== "function") throw TypeError; function f() {}; //定义一个空构造函数 f.prototype = p; //将其原型属性设置p return new f(); //将f()创建p的继承对象 } var o = {} //o从Object.prototype继承对象的方法 o.x = 1; //给o定义一个属性x var p = inherit(o); //p继承o和Object.prototype p.y = 2; //给p定义一个属性y var q = inherit(p); //q继承p、o和Object.prototype q.z = 3; //给q定义一个属性z var s = q.toString(); //toString()继承Object.prototype q.x + q.y //=>3:x和y分别继承自o和p
属性赋值操作首先检查原型链,以此判断是否允许赋值操作。例如:o继承自一个只读属性x,那么赋值操作是不允许的。如果允许属性赋值操作,它也总是在原始对象上创建属性或对已经有的属性赋值,而不会修改原型链。在javascript中,只有在查询属性时才能体会到继承的存在,而设置属性和继承无关,这也是javascript的一个重要特性,该特性让程序员有选择地覆盖(override)继承的属性。
q.x = 5 o.x //1
属性的值要么失败,要么创建一个属性,要么在原始对象中设置属性。但有一个例外,如果o继承属性x,而这个属性是一个具有setter方法的accessor属性(参照6.6),那么在这时调用的settter方法而不是给o创建一个属性x.需要注意的是,setter方法是由对象o调用的,而不是定义属性的原型对象调用的。因此,如果setter方法定义任意属性,这个操作只是征对o本身,不会修改原型链。
2.3.属性访问错误
属性访问并不返回或设置一个值。
查询一个不存在的属性不会报错,如果对象o自身的属性或继承的属性中均未找到属性x,属性的访问表达式o.x返回dundefined。
但是,如果对象不存在,那么试图查询这个不存在的对象就会报错,null和undefined值都没有属性,因此查询这些值会报错:
//抛出一个类型错误异常,undefined没有length属性 var len = book.subtitle.length;
除非确定book和subtitle都是(或在行为上)对象,否则不能写成book.subtitle.length,否则会报错,下面提供了两种避免出错的方法:
var lerr = undefined; if (book){ if(book.subtitle) len =book.subtitle.length; } //一种更简练的常用的方法,获取subtitle的length属性undefined var len =book&&book.subtitle&& book.subtitle.length;
当然给null和undefined设置属性也会报类型错误。给其它值设置属性也不总是成功。一些属性是只读的,不能重新赋值,有一些对象不允许新增属性。但这些属性设置失败时不会 报错。
//内置构造函数的原型是只读的 Object.prototype = o;//赋值失败,但没报错,也没有修改
这个bug在ECMAScript5的严格模式已经修复。严格模式下,失败的属性设置操作都会抛出类型错错误的异常。
尽管属性的赋值成功或失败规律看起来很简单,但是要描述清楚并不容易,在下面的场景中,给对象o设置属性p会失败:
- o的属性p是只读的,不能给只读属性从新赋值(defineProperty()方法中只有一个例外, 可以对可配置的只读属性从新赋值。查看该方法)
- o的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性。
- o中不存在自有属性p:o没有使用setter方法继承属性p.并且o的可扩展性(extensible attrubute)是false(6.8.3).如果o中不存在p,而且没有setter方法可调用,则p一定会添加到o中。但如果o不是可扩展的,那么o不能定义新属性。
3.删除属性
delete运算符可以删除对象的属性。它的操作数应当是一个属性访问表达式,让人感到意外的是,delete只是断开属性和宿主对象的联系,而不会操作属性中的属性。
delete book.author; //book不再有属性author delete book.["main title"] //book不再有属性"main title"
注:a={p:{x:1}}; b=a.p;delete a.p执行这段代码后,b.x的值仍然是1.由于已经删除的属性引用依然存在,因此在javascript的某些实现中,可能因为这种不严谨而造成内存泄漏,所以在销毁对象的时候,要遍历属性中的属性,依次删除。
delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它, 而且这会影响到所有继承这个原型的对象)。
当delete表达式删除成功或没有任何副作用(比如删除不存在的属性)时,它返回true。如果delete后不是一个属性访问表达式,delete同样返回true:
o = {x:1}; //o有一个属性x,并继承属性toString delete o.x; //删除x,返回true delete o.x; //什么都没做,(x已经不存在),返回true delete o.toString(); //什么也没有做(toString是继承来的),返回true delete o.toString(); //返回true delete 1; //无意义
delete不能删除那些可配置性为false的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建全局对象的属性。在严格模式中,删除一个不可配置的属性也会报一个类型错误。在非严格模式中,这些情况下的delete操作也会返回false
delete Object.prototype; //不能删除,属性是不可配置的 var x = 1; //声明一个全局变量 console.log(delete this.x); //不能删除整个属性 function f() {} // 声明一个全局函数 console.log(delete this.f); //也不能删除全局函数
当在非严格模式中删除全局对象的可配置值属性时,可以省略对全局对象的引用,直接在delete操作符后跟随要删除的属性名即可:
this.x = 1; //创建一个可配置的全局属性(没有用var) delete x; //将它删除
然而在严格模式下,delete后跟随一个非法的操作数(比如x),则会抱一个语法错误,因此必须显示指定对象及其属性:
delete x; //在严格模式下报语法错误 delete this.x; //正常工作
4.检测属性
javascript对象可以看做是属性的集合,我们经常会检测集合中成员的所属关系--判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnProperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也做到这一点。
in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true:
var o = {x: 1} "x" in o; //=>true "y" in o; //=>false y不是o的属性 "toString" in o;//=>true o继承toString属性
对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性,对于继承属性它将返回false:
var o = {x: 1}; o.hasOwnProperty("x"); //=>true o中有一个自有属性x o.hasOwnProperty("y"); //=>false o.hasOwnProperty("toString"); //false toString是继承属性
propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(Enumerable attrubute)为true时才返回true。某些内置属性是不可枚举的。通常javascript代码创建的属性都是可枚举的,除非在ECMAScript5中使用一个特殊的方法来改变属性的可枚举性
var o = inherit({y: 2}); o.x = 1; o.propertyIsEnumerable("x"); //true:o是一个可枚举的自有属性 o.propertyIsEnumerable("y");//false y是继承来的 Object.prototype.propertyIsEnumerable("toString");//false 不可枚举
除了使用in运算符之外,另一种更简便的方法是使用"!=="判断一个属性是否是undefined:
var o = {x: 1} o.x !== undefined; //true o有属性x o.y !== undefined; //fakse: o没有属性y o.toString() !== undefined; //true:o继承了toString属性
然而有一种场景只能用in运算符不能用上述的属性访问方式。in可以区分不存在的属性和存在值为undefined的属性。例如下面的代码:
var o = { x: undefined } // o.x !== undefined; //属性存在,但值为undefined o.y !== undefined; //属性不存在 "x" in o; //true "y" in o; //false delete o.x; //删除了属性x "x" in o //false 属性不存在
注意,上述代码中使用的是"!==",而不是"!="。"!=="可以区分undefined和null.有时则不必做这种区分:
//如果o 中有属性x,且x的值不是null undefined ,o.x乘以2 if (o.x = null) o.x *= 2; //如果o中还有属性x, 且x的值不能转换false, o.x乘以2 //如果x是undefined、null、false、""、0、NaN、则它保持不变 if (o.x) o.x *= 2;
5.枚举属性
除了检测对象属性是否存在,我们还会经常遍历对象的属性。通常使用for/in循环遍历,ECMAScript5提供了两个更好的替代方案。
for/in循环在循环体中遍历所有可枚举的属性(包括自有属性和继承属性),把属性名称赋值给循环变量。
var o = {x: 1,y: 2,z: 3};//三个可枚举的属性 o.propertyIsEnumerable("toString");//=>false,不可枚举 for(p in o)//遍历属性 console.log(p); //属性x y z 不会输出toString
有很多实用工具库给Object.prototype添加了新的方法或属性,这些方法和属性可以被对象继承并使用。然后在ECMAScript5标准之前,这些新添加的方法是不能定义为不可枚举的,因此他们都可以在for/in循环中枚举出来。为了避免这种情况,需要过滤for/in循环返回的属性,下面两种方式是最常见的:
for(p in o){ if(!o.hasOwnProperty(p)) continue; //跳过继承的属性 } for(p in o){ if (typeof o[p]==="function") continue; //跳过方法 }
下面的例子定义了一些有用的工具函数来操控对象的属性,这些函数用到了for/in循环。实际上extend()函数经常出现在javascript实用工具库里
/* *把p中可枚举的属性复制到o中,并返回o *如果o和p中含有同名属性,则覆盖o中的属性 *这个函数并不出来getter和setter以及复制属性 */ function extend(o, p) { for (prop in p) { //遍历p中所有的属性 o[prop] = p[prop]; //将遍历属性添加至o中 } return o; } /*将p中可枚举的属性复制至o中,并返回o * 如果o和p有同名属性,o中的属性将不受影响 * 这个函数并不出来getter和setter以及复制属性 */ function merge(o, p) { for (prop in p) { //遍历p中所有的元素 if (o.hasOwnProperty[prop]) continue; //过滤掉已在o中存在的属性 o[prop] = p[prop]; //将属性添加至o中 } return o; } /* * 如果o中的属性在p中没有同名属性,则从o中删除这个属性 * 返回o */ function restrict(o, p) { for (prop in o) { //遍历o的所有属性 if (!(prop in p)) delete o[prop]; //如果在p中不存在,则删除之 } return o; } /* * 如果o中的属性在p中存在属性,则从o中删除这个属性 * 返回o */ function subtarck(o, p) { for (prop in p) { //遍历p中所有的属性 delete o[prop]; //从o中删除(删除一个不存在的属性一般不报错) } return o; } /* * 返回一个新对象,这个对象同事拥有o的属性和p的属性 * 如果o和p中有同名属性,使用p中的属性 */ function union(o, p) { return extend(extend({}, o), p); } /* * 返回一个新对象,这个对象同事拥有o的属性和p中出现的属性 * 很像求o和p的交集,但p中的属性值被忽略 */ function intersection(o, p) { return restrict(extend({}, o), p); } /* * 返回一个数组,这个数组包含的是o中可枚举的自由属性的名字 */ function keys(o) { if (typeof o !== "object") throw TypeError(); //参数必须是对象 var result = []; //将要返回的对象 for (var prop in o) { //遍历所有可枚举的属性 if (o.hasOwnProperty(prop)) //判断是否自有属性 result.push(prop); //将属性名添加至数组中 } return result; //返回这个数组 }
除了for/in循环外,ECMAScript5定义了两个用以枚举属性名称的函数。一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自由属性的名称组成,它的工作原理和上述例子最后的函数keys()相似。
ECMAScript5中的第二个枚举属性的函数是Object.getOwnPropertyNames(),它和keys()类似,只是它返回的对象的所有自有属性的名称,而不仅仅是可枚举的属性。在ECMAScript3中是无法实现类似的函数的,因为ECMAScript3没有提供任何反复来获取对象的不可枚举属性。
6.属性getter和setter
我们知道,对象的属性是由名字、值或一组特性(attribute)构成的。在ECMAScript5中,属性值可以用一个或两个方法代替。这两个方法就是getter和setter,由getter和setter定义的属性称作“存取器属性”(accessor property),它不同于“数据属性”(data property),数据属性只有一个简单的值。
当程序查询存取器属性的时候,javascript调用getter方法(参数)。这个方法返回的值就是属性存取表达式的值。当程序设置一个存取器属性的值时,javascript调用settter方法,将赋值表达式右侧的值当做参数传入setter。从某种意义上,这个方法负责“设置”属性值。可以忽略setter方法的返回值。
和数据属性不同,存取器属性具有不可写(writable attribute)。如果属性同时具有gettter和settter方法,那么它是一个读写属性。如果它指引getter方法,它是一个可读属性。如果它只有setter方法,那么它是一个只写属性(数据属性中有一些例外),读取只写属性总是返回undefined。
定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法:
var o = { //普通数据属性 data_prop: value; //存取器属性都是成对定义的函数 get accessor_prop() { /*这里是函数体*/ }, set accessor_prop(value) { /*这里是函数体*/ } };
存取器属性定义一个或两个和函数同名的函数,这函数定义没有使用function关键字,而是使用get和(或)set。注意这里没有使用冒号将属性名和函数体分开,但在函数体的结束和下一个方法或数据属性之间有逗号分隔。例如:思考这个表示2D笛卡尔点坐标(笛卡尔坐标系就是直角坐标系和斜角坐标系的统称)的对象。他有两个普通属性x和y,分别表示对于点的x坐标和y坐标,它还有两个等价的存取器坐标用来表示店的极坐标:
var p = { //x和y是普通的可读写数据属性 x: 1.0, y: 1.0, //r是可读写的存取器属性,它带有getter和setter。 //函数体结束后不要忘记带上逗号 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; }, //theta是只读存取器属性,它只有getter方法 get theta() { return Math.atan2(this.y, this.x); } };
和数据属性一样,存取器属性是可以继承的,因此上述代码中的对象p可以当成令一个“点”的原型。可以给新对象定义它的x和y属性,但r和theta属性是继承来的:
var q = inherit(p); //创建一个继承getter和setter的新对象 q.x = 1, q.y = 1; //给q添加两个属性 console.log(q.r) //可以使用存储器的存取属性 console.log(q.theta);
这段代码使用存取器属性定义API,API提供了表示同一组数据的两种方法(笛卡尔坐标系和极坐标西表示法)。还有很多场景用到存取器属性,比如智能检测属性的写入值以及在每次属性读取时返回不同值:
//这个对象产生严格的自增序列号 var serialnum = { //这个属性包含下一个序列号 //$符号暗示这个属性是一个私有属性 $n: 0, //返回当前的值,然后自增 get next() { return this.$n++; }, //返回当前新的值,大只有当它比当前值大时才设置成功 set next(n) { if (n >= this.$n) this.$n = n; else throw "序列号的值不能比当前值小"; } };
我们再看一个例子,这个例子使用getter方法实现一种“神奇”的属性
//这个对象表示有一个可以返回随机数的存取器属性 //例如,"random.octet"产生一个随机数 //每次产生的随机数都在0-255之间 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; } }
本节简单介绍了如何给对象直接量定义存取器属性。下节会介绍如何给一个已经存在的对象添加一个存取器属性。
7.属性的特征
除了包含名字和值之外,属性还包含一些标识它们可写,可枚举和可配置的特征。在ECMAScript3中无法设置这些特性,所有通过ECMAScript3的程序创建的属性都是可写的,可枚举的,可配置的,且无法对这些特性做修改。本节讲解ECMAScript5中查询和设置这些属性的API,这些API对于库开发者来说非常重要,因为
- 可以通过这些API给原型对象添加方法,并将它们设置成不可枚举的,这让它们看起来更新内置方法。
- 可以通过这些API给对象定义不能修改或删除的属性,借此“锁定”这个对象
在本节里,我们将存取器属性getter和setter方法看成是属性的特性。按照这个逻辑,我们也可以把数据属性的值同样看成属性的特征。因此,可以认为一个属性包含一个名称和四个特性。数据属性的4个特性分别是它的值(value),可写性(writable)、可枚举性(enumerable)、和可配置性(configurable)。存取器属性不具有值(value)特性和可写性,它们的可写性是由setter方法存在与否来决定的。因此,存取器属性的4个特性是读取(get)、写入(set)、可枚举性和可配置性。
为了实现属性特征的查询和设置操作,ECMAScript5中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那4个特性。描述符对象的属性和他们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有value、writable、enumerable、configurable。存取器属性的描述符对象则用get属性和set属性代替value和writable。其中,writable、enumerable、configurable都是布尔值,当然get属性和set属性是函数值。
通过调用Object.getOwnPropertyDescriptor()可以获得某个对象特定的属性描述符:
//返回{value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor({x: 1}, "x"); //查询上例子中的randam对象的octet属性 //返回 {get: /*function octet(){...*/ , set: undefined, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(random, "octet"); //对于继承属性和不存在的属性,返回undefined Object.getOwnPropertyDescriptor({}, "x"); //undefined 没有这个属性 Object.getOwnPropertyDescriptor({}, "toString"); //undefined 继承属性
从函数名就可以看出,Object.getOwnPropertyDescriptor()只能得到自有属性的描述符。要想获得继承属性的特性,需要遍历原型链(参照6.8.i Object.getPrototypeof())。
要想设置属性的特性,或者想让新建的属性具有某种特性,则需要调用Object.defineProperty(),传入要修改的对象、要创建或修改的属性名的名称以及属性描述符对象
var o = {}; //空对象 //添加一个不可枚举的数据属性x,并赋值1 Object.defineProperty(o,"x",{value:1,writable:true,enumerable:false,configurable:true}); // {value: 1, writable: true, enumerable: false, configurable: true} Object.getOwnPropertyDescriptor(o,"x"); //属性是存在的,但不可枚举 o.x; //=> 1 Object.keys(o) //=> []; //现在对属性x修改,让它只变为只读 Object.defineProperty(o,"x",{writable:false}); //试图改变这个属性的值 o.x=2; //操作失败但不报错,严格模式中会抛出类型错误的异常 o.x //=> 1 //属性依然是可配置的,因此可以通过这样的方式进行修改: Object.defineProperty(o,"x",{value:2}); o.x //=>2 //现在讲x从数据属性修改为存取器属性 Object.defineProperty(o,"x",{get: function(){return o;} }); o.x //=>o
传入Object.defineProperty()的属性描述符对象不必包含4个所有特性。对于新创建的属性来说,默认的特性值是false或undefined。对于修改已有的属性来说。默认的特性值没有做过任何修改。注意:这个方法要么修改已有属性要么新建自有属性,但不能修改继承属性。
对于同时修改或创建多个属性,则需要使用Object.defineProperties(),第一个参数是要修改的对象,第二个参数是一个映射表,它包含要新建或修改的属性名称,以及他们的属性描述符,例如:
var p = Object.defineProperties({},{ x:{value:1,writable:true,enumerable:true,configurable:true}, y:{value:1,writable:true,enumerable:true,configurable:true}, z:{ get:function(){ return Math.sqrt(this.x*this.x + this.y*this.y)},enumerable:true,configurable:true } }); console.log(p) //=>Object {x: 1, y: 1, z: 1.4142135623730951}
这段代码从一个空对象开始,给它添加两个数据属性和一个存取器属性(只读),最终,Object.defineProperties()返回修改后的对象和Object.defineProperty()一样。
对于那些不允许创建或修改的属性来说,使用Object.defineProperty()和Object.defineProperties()对其操作(新建或修改)就会抛出类型错误异常,比如,给一个不可扩展的对象(6.8.iii)新增属性就会抛出类型错误异常。造成这些方法抛出类型错误异常的其它原因则和本身相关。
可写控制着对值特性的修改。可配置性控制着对其它特性(包括属性是否可以删除)的修改。然而规则远不止这么简单。
例如如果属性可配置的话,则可以修改不可写的属性。下面是完整的规则,任何对Object.defineProperty()或Object.defineProperties()违反规则的使用都会抛出类型错误的异常。
- 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性
- 如果属性是不可配置的,则不能修改它的可配置性和和可枚举性
- 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能将它转换为数据属性
- 如果数据属性是不可配置的,则不能将它转换为存取器属性
- 如果数据属性是不可配置的,则不能将它的可写性从false修改为true,但可以从true修改为false。
- 如果数据属性是不可配置且不可写的,则不能修改它的值。然后可配置但不可写属性值是可以修改的(实际上将它先标记为可写的,然后修改它的值最后转换为不可写的)
例如上文的extend()函数,这个函数将一个对象的属性复制到另外一个对象中。这个函数只是简单地复制属性名和值,并没有复制属性的特性,而且也没有复制存取器属性的getter和setter方法,只是将它们简单的转化为静态的数据属性。
下面的例子给出了更健壮的extend(),它使用Object.defineProperty()和Object.defineProperties()对属性的所有特性进行复制。新的extend()作为不可枚举属性添加到Object.prototype中,因为它是Object上定义的新方法,而不是一个独立的函数。
/* *复制属性的特性 *Object.prototype添加一个不可枚举的extend()方法 * 这个方法继承自调用它的对象,将作为参数传入的对象的属性一一复制 * 除了值之外,也复制属性的所有特性,除非在目标对象中有同名的属性 * 参数对象的所有自有对象(包括不可枚举的属性)也会一一复制 * */ Object.defineProperty(Object.prototype, "extend", //定义Object.prototype.extend { writable: true, enumerable: false, //将其定义为不可枚举的 configurable: true, value: function(o) { //值就是这个函数 //得到所有的自由属性,包括不可枚举属性 var names = Object.getOwnPropertyNames(o); //遍历他们 for (var i = 0; i < names.length; i++) { //如果属性已经存在,则跳过 if (names[i] in this) continue; //获得o中的属性描述符 var desc = Object.getOwnPropertyDescriptor(o, names[i]); //用它给this创建一个属性 Object.defineProperty(this, names[i], desc); } } } );
getter和setter的老式API
可以通过6.6节的对象直接量语法诶新对象定义存取器属性,但不能查询属性的getter和setter方法或给已有的对象添加新的存取器属性。在ECMAscript5中,可以通过Object.getOwnPropertyDescriptor()和Object.defineProperty()来完成这项工作
在ECMAScript5标准采纳之前,大多数的javascript的实现(ie浏览器除外)已经可以来支持对象直接量中的get和set写法。这些非标准的老式API用来查询和设置getter和setter。这些API由4个方法组成,所有对象有拥有这些方法。__lookupGetter__()和__lookupSetter__用以返回一个命名属性getter和setter方法。__defineGetter__()和__defineSetter__用以定义getter和setter方法,两个函数的第一个参数是属性名字,第二个参数是getter和setter方法、这四个方法都是以两条下划线做前缀,两条下划线做后缀,以表明他们是非标准的方法。
8.对象的三个属性
每一个对象都有与之相关的原型(protype)、类(class)、和可扩展性(extensible attribute)。
8.1.原型属性
对象的原型属性是用来继承属性的。可以直接叫做“原型”。
原型属性是在实例对象创建之初就设置好的。通过对象直接量创建的对象使用Object.prototype作为他们的原型。通过new创建的对象使用构造函数的protype属性作为它们的原型。通过Object.create()创建的对象使用第一个参数(也可能是null)作为他们的原型。
在ECMAScript5中,将对象作为参数传入Object.getPrototypeOf()可以查询它的原型。在ECMAScript3中,没有与之等价的函数,但经常使用o.constructor.prototype来检测一个对象的原型。通过new表达式创建的对象,通常继承一个constructor属性。这个属性知道创建这个对象的构造函数。
注意,通过对象直接量或Object.create()创建的对象包含一个名为constructor的属性,这个属性代指Object()构造函数。因此,constructor.prototype才是对象直接量的真正的原型,但通过Object.create()创建的对象则往往不是这样。
要检测一个对象是否是另一个对象的原型(或处于原型链中),请使用isPrototypeOf()方法。例如通过p.isPrototypeOf(o)用来检测p是否为o的原型。
var p = {x: 1}; //一个原型对象 var o = Object.create(p); //使用这个原型创建一个对象 p.isPrototypeOf(o); //=>true o继承自p Object.prototype.isPrototypeOf(p); //=>true p继承自Object.prototype
需要注意的是,isPrototypeOf()函数实现的功能和instanceof运算符非常类似
Mozilla实现的javascript包括早年的netcscape对外暴露了一个命名为__proto__的属性,用以查询和设置对象的原型。但不推荐使用它,尽管safari和chrome的当前版本都支持它,在IE和opera都未实现它(可能以后也不实现)。实现了ECMASCript5的firefox版本也支持__proto__。但对修改不扩展的对象的原型做了限制。
8.2.类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ECMAScript3和5都没有提供设置这个属性的方法,并且只有一种间接的方法可以查询它。
默认的toString()方法(继承自Object.prototype)返回了如下这种格式的字符串。
[object class]
因此,要获得对象的类,可以调用对象的toString()方法,然后提取已返回的字符的第8个到倒数第2个字符之间的字符。不过让人棘手的是:很多对象toString()方法重写了。为了能调用正确的toString()版本,必须间接的使用function.call()方法(8.3.iii)。例如下面的classOf()就可以传回传递给它的任意对象的类。
function classOf(o) { if (o === null) return "Null"; if (o === undefined) return "Undefined"; return Object.prototype.toString.call(o).slice(8, -1); }
classOf()函数可以传入任何类型的参数。数字、字符串、布尔值可以直接调用toString()方法,就和对象调用toString()方法一样(实际上是这些类型的变量调用toString方法,而不是通过他们的直接量调用toString()。比如1.toString()是不对的,而是先声明var a =1,然后调用a.toString()),并且这个函数包含了对null和undefined的特殊处理(在ECMAScript5中不需要对这些特殊情况做处理)。通过内置构造函数(比如Array和Date)创建的对象包含“类属性”(class attribute),它与构造函数名称相匹配。宿主对象也包含有意义的“类属性”,但这和具体的javascript实现有关。通过对象直接量和Object.create创建的对象的属性是"Object",那些自定义构造函数创建的对象也是一样,类型属性也是 "Object",因此,对于自定义的类来说,没法通过类属性区分对象的类。
classOf(null) //=>"Null" classOf(1) //=>"Number" classOf("") //=>"String" classOf(false) //=>"blooean" classOf({}) //=>"Object" classOf([]) //=>"Array" classOf(/./) //=>"RegExp" classOf(new Date()) //=>"Date" classOf(window) //=> "window"(这是客户端宿主对象) function f() {} //定义一个自定义构造函数 classOf(new f()) //=> "Object"
8.3.可拓展性
对象的可扩展性用以表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显式扩展的,宿主对象的可扩展性是由javascript引擎定义的。在ECMAScript5中,所有的内置对象和自定义对象都是可扩展的,除非将它转换为不可扩展的。同样宿主对象的可扩展性也是由实现的ECMAScript5的javascript引擎定义的。
ECMAScript5定义了用来查询和设置对象的可扩展函数。通过对象传入Object.esExtensible(),来判断该对象是不是可以扩展的。如果想将对象转换为不可扩展的,需要调用Object.preventExtensions(),将待转换的对象作为参数传进去。注意:将对象转换为不可扩展的,就无法将其再转换回可扩展的了。同样需要注意的,Object.preventExtensions()只影响到对象本身的扩展性。如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。
可扩展属性的目的是将对象“锁定”,以避免对外界的干扰。对象的可扩展性通常和属性的可配置性与可写性配合使用,ECMAScript5定义了一些函数可以更方便的设置多种属性。
Object.seal()和Object.preventExtensions()类似,除了能将对象设置为不可扩展的,还可以将将对的所有自有属性都设置为不可配置的,也就是说,不能给这个对象添加新属性,而且它已有的属性也不能删除和配置,不过它已有的可写属性依然可以配置。对于那些已经封闭(sealed)起来的对象是不能解封的。可以使用Object.isSealed()来检测是否封闭。
Object.freeze()将更严格地锁定对象----"冻结"(frozen)。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它的自由所有数据属性设置为只读(如果对象的存取器属性具有setter方法,存取器属性将不受影响,仍然可以通过给属性赋值调用他们)。使用Object.isFrozen()来检测是否冻结对象。
Object.preventExtensions()、Object.seal()、Object.freeze()都返回传入的对象,也就是说,可以通过函数嵌套的方法调用它们。
//创建一个封闭对象,包括一个冻结的原型和一个不可枚举的属性 var o = Object.seal(Object.create(Object.freeze({x: 1}), { y: {value: 2,writable: true} }));
9.序列化对象
对象序列化(serialization)是将对象的状态转换为字符串,也可以将字符串还原为对象。ECMAScript5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原javascript对象。这些方法都使用JSON作为数据交换格式,JSON的全称是"Jvascript Object Notation" ——javascript对象表示法,它的语法和javascript对象与数组直接量的语法非常接近:
o = {x: 1,y: {z: [false, null, ""]}}; //定义一个测试对象 s = JSON.stringify(o); //s是'{"x":1,"y":{"z":[false,null,""]}}' p = JSON.parse(s) //p是o的深拷贝
ECMAScript5中这些函数本地实现和http://json.org/json2.js(http://json.org/json-zh.html)中公共域ECMAScript3版本的实现非常相似,或者说完全一样,因此可以通过引用json2.js模块在ECMAScript5的环境中使用这些函数。
JSON语法是javascript语法的子集,它并不能表示javascript里的所有的值。支持对象、数组、字符串、无穷大数字、true、false和null,并且他们可以序列化和还原。NaN、Infinity和-Infinity序列化的结果是null,日期对象序列化的结果是ISO格式的日期日期字符串(参照Date.toJSON()函数),但JSON.parse()依然保留他们的字符串形态,而不会还原为原始日期对象。函数、RegExp、Error对象和undefined值不能序列化和还原。JSON.stringify()只能序列化对象可枚举的自有属性。对于一个不能序列化的属性来说,在序列化后输出字符串中会将这个属性省略掉。JSON.stringify()和JSON.parse()都可以接收第二个可选参数,通过传入要序列化或还原的的属性列表来定制自定义的序列化或还原操作。第三部分会有关于这些函数的详细文档。
10.对象方法
上文描述过,所有的javascript对象都是从Object.prototype继承属性(除了那些不通过原型显式创建的对象)。这些继承属性主要是方法,因为javascript程序员普遍对继承方法更感兴趣。我们已经讨论过hasOwnProprtty()、propertyIsRnumerable()、isPrototypeOf()这三个方法,以及在Object构造函数里定义的静态函数Object.create()和Object.getPrototypeOf()的等。本节将对定义在Object.prototype里的对象方法展开讲解。
10.1.toString()方法
toString没有参数,它将返回一个表示调用这个方法对象值的字符串。在需要将对象转换为字符串的时候,javascript会调用这个这个方法。比如,使用“+”号连接一个字符串和一个对象时或者在希望使用字符串的方法中使用了对象时都会调用toString()
默认的toString()方法在返回值带有的信息量很小,例如下面的代码计算结果为 [object Object]
var s ={x:1,y:1}.toString(); //=>[object Object]
由于默认的toString()方法并不会输出很多有用的信息,因此很多类有带有自定义的toString()。例如,将数组转换为字符串时,结果是一个元素列表,只是每个元素都转换成了字符串。再比如,当函数转换为字符串时,得到函数的源代码。第三部分有关于toString()详细文档说明。比Array.toString(),Date.toString以及function.toString().
10.2.toLocaleString()方法
除了基本的toString()方法之外,对象都包含toLocaleString()方法,这个方法返回一个表示这个对象本地化的字符串。
Object中默认的toLocaleString()方法并不做任何本地化的自身的在,它仅调用toString方法返回对应值。Date和Number类对toLocaleString()方法做了定制,它可以对数字、日期和时间做本地化转换。Array类的toLocaleString()方法和toString方法很像,唯一不同的是每个数组元素会调用toLocaleString()方法转换为字符串,而不是调用各自的toString()方法。
10.3.toJSON()方法
Object.prototype实际上没有定义toJSON()方法,但对于要执行序列化的对象来说,JSON.stringify()方法会调用到toJSON()方法。如果在待序列化的对象中存在这个方法则调用它,返回值即是序列化的结果,而不是原始的对象。具体示例参加Date.toJSON()。
10.4.valueOf()方法
valueOf()和toString()方法非常类似,但往往javascript需要将对象转换为某种原始值而非字符串的时候才调用它,尤其是转换为数字的时候。如果在需要使用原始值的上下文中使用了对象,javascript就会自动调用这个对象。默认的valueOf()方法不足为奇,但有些内置自定义了valueOf()方法(比如Date.valueOf())。