对象是JavaScript的基本数据类型。对象是一种复合值:它将很多值(原始值或者其他对象)聚合在一起,可以通过名字访问这些值。对象也可看作是属性的无序集合,每个属性都是一个名值对,属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。
JavaScript对象还可以从一个称为原型的对象继承属性,对象的方法通常是继承的属性,这种原型式继承(prototypal inheritance
)是JavaScript的核心特征。
对象最常见的用法是创建(create
)、设置(set
)、查找(query
)、删除(delete
)、检测(test
)和枚举(enumerate
)它的属性。属性包括名字和值,属性名可以是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性,值可以是任意JavaScript值,或者可以是一个getter
或setter
函数。除了名字和值以外,每个属性还有一些与之相关的值,称为“属性特征”(property attribute
)。
可写(
writable attribute
),表明是否可以设置该属性的值。可枚举(
enumerable attribute
),表明是否可以通过for/in
循环返回该属性。可配置(
configurable attribute
),表明是否可以删除或修改该属性。
除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute
):
对象的原型(
prototype
)指向另外一个对象,本对象的属性继承自它的原型对象。对象的类(
class
)是一个标识对象类型的字符串。对象的扩展标记(
extensible flag
)指明了是否可以向该对象添加新属性。
最后,我们用下面这些术语来对三类JavaScript对象和两类属性作区分:
内置对象(
native object
)是由ECMAScript规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。宿主对象(
host object
)是由JavaScript解释器所嵌入的宿主环境(比如Web浏览器)定义的。客户端JavaScript中表示网页结构的HTMLElement
对象均是宿主对象。既然宿主环境定义的方法可以当成普通的JavaScript函数对象,那么宿主对象也可以当成内置对象。自定义对象(
user-defined object
)是由运行中的JavaScript代码创建的对象。自有属性(
own property
)是直接在对象中定义的属性。继承属性(
inherited property
)是在对象的原型对象中定义的属性。
一、创建对象
可以通过对象直接量、关键字new
和Object.create()
函数来创建对象。
1. 对象直接量
创建对象最简单的方式就是在JavaScript代码中使用对象直接量。
1 | var emtpy = {}; |
对象直接量是一个表达式,这个表达式的每次运算都会创建并初始化一个新的对象。每次计算对象直接量的时候,也都会计算它的每个属性的值。也就是说,如果在一个重复调用的函数中的循环体内使用了对象直接量,它将创建很多新对象,并且每次创建的对象的属性值也有可能不同。
2. 通过new创建对象
new运算符创建并初始化一个新对象。关键字new
后跟随一个函数调用。这里的函数称作构造函数(constructor
),构造函数用以初始化一个新创建的对象。JavaScript语言核心中的原始类型都包含内置构造函数。
1 | var o = new Object(); |
3. 原型
每一个JavaScript对象(null
除外)都和另一个对象相关联。“另一个”对象就是我们熟知的原型,每一个对象都从原型继承属性。
所有通过对象直接量创建的对象都具有同一个原型对象,并可以通过JavaScript代码Object.prototype
获得原型对象的引用。通过关键字new
和构造函数调用创建的对象的原型就是构造函数的prototype
属性的值。因此,同使用{}
创建对象一样,通过new Object()
创建的对象也继承自Object.prototype
。同样,通过new Array()
创建的对象的原型就是Array.prototype
,通过new Date()
创建的对象的原型就是Date.prototype
。
没有原型的对象不多,Object.prototype
就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自Object.prototype
的原型。例如,Date.prototype
的属性继承自Object.prototype
,因此由new Date()
创建的Date
对象的属性同时继承自Date.prototype
和Object.prototype
。这一系列链接的原型对象就是所谓的“原型链”(prototype chain
)。
4. Object.create()
ECMAScript5定义了一个Object.create()
方法,它创建一个新对象,其中第一个参数是这个对象的原型,并提供第二个可选参数,用以对对象的属性进行进一步描述。
1 | // o1继承了属性x和y |
可以通过传入参数null
来创建一个没有原型的新对象,但通过这种方式创建的对象不会继承任何东西,甚至不包括基础方法,比如toString()
,也就是说,它将不能和“+”
运算符一起正常工作
1 | // o2不继承任何属性和方法 |
如果想创建一个普通的空对象(比如通过{}
或new Object()
创建的对象),需要传入Object.prototype
1 | // o3和{}和new Object()一样 |
可以通过任意原型创建新对象(换句话说,可以使任意对象可继承),这是一个强大的特性。
1 | // inherit()返回一个继承自原型对象p的属性的新对象 |
注意,
inherit()
方法并不能完全代替Object.create()
,它不能通过传入null
原型来创建对象,而且不能接收可选的第二个参数。
inherit()
函数的其中一个用途就是防止库函数无意间(非恶意地)修改那些不受控制的对象。不是将对象直接作为参数传入函数,而是将它的继承对象传入函数。当函数读取继承对象的属性时,实际上读取的是继承来的值。如果给继承对象的属性赋值,则这些属性只会影响这个继承对象自身,而不是原始对象。
1 | var o = { x: "don't change this value" }; |
二、属性的查询和设置
对象可以通过点(.
)或方括号([]
)运算符来获取属性的值。运算符左侧应当是一个表达式,它返回一个对象。对于点(.
)来说,右侧必须是一个以属性名称命名的简单标识符。对于方括号来说([]
),方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名字
1 | var author = book.author; |
当使用方括号时,严格来讲,表达式必须返回字符串或者返回一个可以转换为字符串的值
1 | var author = { |
1. 继承
JavaScript对象具有“自有属性”(own property
),也有一些属性是从原型对象继承而来的。假设要查询对象o
的属性x
,如果o
中不存在x
,那么将会继续在o
的原型对象中查询属性x
。如果原型对象中也没有x
,但这个原型对象也有原型,那么继续在这个原型对象的原型上执行查询,直到找到x
或者查到一个原型是null
的对象为止。可以看到,对象的原型属性构成了一个“链”,通过这个“链”可以实现属性的继承。
1 | var o = {}; // o从Object.prototype继承对象的方法 |
现在假设给对象o
的属性x
赋值,如果o
中已经有属性x
(这个属性不是继承来的),那么这个赋值操作只改变这个已有属性x
的值。如果o
中不存在属性x
,那么赋值操作给o
添加一个新属性x
。如果之前o
继承自属性x
,那么这个继承的属性就被新创建的同名属性覆盖了。
属性赋值操作首先检查原型链,以此判定是否允许赋值操作。例如,如果o
继承自一个只读属性x,那么赋值操作是不允许的。如果允许属性赋值操作,它也总是在原始对象上创建属性或对已有的属性赋值,而不会去修改原型链。在Javascript中,只有在查询属性时才会体会到继承的存在,而设置属性则和继承无关,这是JavaScript的一个重要特性,该特性让程序员可以有选择地覆盖(override
)继承的属性。
1 | var unitcircle = { r: 1 }; |
属性赋值要么失败,要么创建一个属性,要么在原始对象中设置属性,但有一个例外,如果o
继承自属性x
,而这个属性是一个具有setter
方法的accessor
属性,那么这时将调用setter
方法而不是给o
创建一个属性。需要注意的是,setter
方法是由对象o
调用的,而不是定义这个属性的原型对象调用的。因此如果setter
方法定义在原型上,这个操作只针对o
本身,并不会修改原型链。
2. 属性访问错误
属性访问并不总是返回或设置一个值。查询一个不存在的属性并不会报错,如果在对象o自身的属性或继承的属性中均为找到属性x,属性访问表达式o.x
返回undefined
1 | book.subtitle; // => undefined,属性不存在 |
但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错,null
和undefined
值都没有属性
1 | var len = book.subtitle.length; // 抛出类型错误异常,undefined没有length属性 |
除非确定book
和book.subtitle
都是对象,否则不能这样书写,应采用如下方法避免出错
1 | // 方法一 |
当然,给null
和undefined
设置属性也会报类型错误,给其他值设置属性也不总是成功,有一些属性是只读的,不能重新赋值,有一些对象不允许新增属性,但让人颇感意外的是,这些设置属性的失败操作不会报错
1 | Object.prototype = 0; // 赋值失败,但没报错,Object.prototype没有修改 |
严格模式下,任何失败的属性设置操作都会抛出一个类型错误异常。
在以下场景下给对象o
设置属性p会失败:
o
中的属性p是只读的:不能给只读属性重新赋值(defineProperty()
方法中有一个例外,可以对可配置的只读属性重新赋值)。o
中的属性p是继承属性,且它是只读的:不能通过同名自有属性覆盖只读的继承属性。o
中不存在自有属性p
:o
没有使用setter
方法继承属性p
,并且o
的可扩展性(extensible attribute
)是false
。如果o
中不存在p
,而且没有setter
方法可供调用,则p
一定会添加至o
中。但如果o
不是可扩展的,那么在o
中不能定义新属性。
三、删除属性
delete
运算符可以删除对象的属性。让人感到意外的是,delete
只是断开属性和宿主对象的联系,而不会去操作属性中的属性
1 | delete book.author; |
delete
运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。
当delete
表达式删除成功或没有任何副作用(比如删除不存在的属性)时,它返回true
。如果delete
后不是一个属性访问表达式,delete
同样返回true
1 | o = {x: 1}; |
delete
不能删除那些可配置性为false
的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。
在非严格模式中,以下情况的delete
操作会返回false
:
1 | // 不能删除,属性是不可配置的 |
当在非严格模式中删除全局对象的可配置属性时,可以省略对全局对象的引用,直接在delete
操作符后跟随要删除的属性名即可
1 | // 创建一个可配置的全局属性(没有用var) |
然而在严格模式下,delete
后跟随一个非法的操作数(比如x),则会报一个语法错误,因此必须显式指定对象及其属性
1 | // 严格模式下报语法错误 |
四、检测属性
JavaScript对象可以看作属性的集合,我们经常会检测集合中成员的所属关系——判断某个属性是否存在于某个对象中。可以通过in
运算符、hasOwnProperty()
和propertyIsEnumerable()
方法来完成这个工作,甚至仅通过属性查询也可以做到这一点。
in
运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true
1 | var o = { x: 1 } |
对象的hasOwnProperty()
方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将会返回false
1 | var o = { x: 1 } |
propertyIsEnumerable()
是hasOwnProperty()
的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute
)为true
时它才返回true
。某些内置属性是不可枚举的。通常由JavaScript代码创建的属性都是可枚举的,除非在ECMAScript5中使用一个特殊的方法来改变属性的可枚举性
1 | var o = inherit({ y: 2 }); |
除了使用in运算符之外,另一种更简便的方法是使用“!==”
判断一个属性是否是undefined
1 | var o = { x: 1 }; |
但是有一种场景只能使用in运算符而不能使用上述属性访问的方式。in可以区分不存在的属性和存在但值为undefined
的属性
1 | var o = { x: undefined } |
注意,上述代码中使用的是
“!==”
运算符,而不是“!=”
。“!==”
可以区分undefined
和null
五、枚举属性
除了检测对象的属性是否存在,我们还会经常遍历对象的属性。通常使用for/in
循环便利,ECMAScript5提供了两个更好用的替代方案。
for/in
循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的(除非使用下文中提到的一个方法将它们转换为不可枚举的)。
1 | var o = { x: 1, y: 2, z: 3 }; |
有许多实用工具库给Object.prototype
添加了新的方法或属性,这些方法和属性可以被所有对象继承并使用。然而在ECMAScript5之前,这些新添加的方法是不能定义为不可枚举的,因此它们都可以在for/in
循环中枚举出来,因此需要过滤for/in
循环中的属性
1 | for (p in o) { |
下方工具函数用来控制对象的属性,这些函数用到了for/in
循环。实际上extend()
函数经常出现在JavaScript实用工具库中
1 | /* |
1 | /* |
1 | /* |
1 | /* |
1 | /* |
1 | /* |
1 | /* |
除了for/in
循环之外,ECMAScript5定义了两个用以枚举属性名称的函数
第一个是
Object.keys()
,它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。第二个是
Object.getOwnPropertyNames()
,它和Object.keys()
类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
六、属性getter和setter
对象属性是由名字、值和一对特性(attribute
)构成的,在ES5中,属性值可以用一个或两个方法替代,这两个方法就是getter
和setter
。由getter
和setter
定义的属性称作“存取器属性”(accessor property
),它不同于“数据属性”(data property
),数据属性只有一个简单的值。
当程序查询存取器属性的值时,JavaScript调用getter
方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript调用setter
方法,将赋值表达式右侧的值当作参数传入setter
。从某种意义上讲,这个方法负责“设置”属性值,可以忽略setter
方法的返回值。
和数据属性不同,存取器属性不具有可写性(writable attribute
)。如果属性同时具有getter
和setter
方法,那么它是一个读/写属性。如果它只有getter
方法,那么它是一个只读属性。如果它只有setter
方法,那么它是一个只写属性(数据属性中有一些例外),读取只写属性总是返回undefined
。
定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法:
1 | var o = { |
和数据属性一样,存取器属性是可以继承的。
1 | var p = inherit(o); |
还有很多场景可以用到存取器属性,比如智能检测属性的写入值以及在每次属性读取时返回不同值
1 | // 这个对象产生严格自增的序列号 |
七、属性的特性
除了包含名字和值之外,属性还包含一些标识它们可写、可枚举和可配置的特性。本节将讲述ES5中查询和设置这些属性特性的API
可以通过这些API给原型对象添加方法,并将它们设置成不可枚举的,这让它们看起来更像内置方法
可以通过这些API给对象定义不能修改或删除的属性,借此“锁定”这个对象
我们将存取器属性的getter
和setter
方法看成是属性的特性,按照这个逻辑,我们也可以把数据属性的值同样看作属性的特性。因此,可以认为一个属性包含一个名字和4个特性。
数据属性的4个特性分别是它的值(value
)、可写性(writable
)、可枚举性(enumerable
)和可配置性(configurable
)
存取器属性不具有值(value
)特性和可写性,它们的可写性是由setter
方法存在与否决定的。因此存取器属性的4个特性是读取(get
)、写入(set
)、可枚举性和可配置性。
为了实现属性特性的查询和设置操作,ES5定义了一个名为“属性描述符”(property descriptor
)的对象,这个对象代表那4个特性。描述符对象的属性有value
、writable
、enumerable
和configurable
。存取器属性的描述符对象则用get
属性和set
属性代替value
和writable
。其中writable
、enumerable
和configurable
都是布尔值,当然,get
属性和set
属性是函数值。
通过调用Object.getOwnPropertyDescriptor()
可以获得某个对象特定属性的属性描述符
1 | // return {value: 1, writable: true, enumerable: true, configurable: true} |
Object.getOwnPropertyDescriptor()
只能得到自有属性的描述符,如果想要获得继承属性的特性,需要遍历原型链。如果想要设置属性的特性,或者想让新建属性具有某种特性,则需要调用Object.defineProperty()
,传入要修改的对象、要创建或修改的属性的名称以及属性描述符对象
1 | var o = {}; |
传入Object.defineProperty()
的属性描述符对象不必包含所有4个特性,对于新创建的属性来说,默认的特性值是false
或undefined
。对于修改的已有属性来说,默认的特性值没有做任何修改。注意,这个方法要么修改已有属性要么新建自有属性,但不能修改继承属性。
如果要同时修改或创建多个属性,则需要使用Object.defineProperties()
。第一个参数是要修改的对象,第二个参数是一个映射表,它包含要新建或修改的属性的名称,以及它们的属性描述符
1 | var p = Object.defineProperties({}, { |
这段代码从一个空对象开始,然后给它添加两个数据属性和一个只读存取器属性。最终Object.defineProperties()
返回修改后的对象(和Object.defineProperty()
一样)。
对于那些不允许创建或修改的属性来说,如果用Object.defineProperty()
和Object.defineProperties()
对其操作就会抛出类型错误异常,比如给一个不可扩展的对象新增属性就会抛出类型错误异常。造成这些方法抛出类型错误异常的其他原因则和特性本身相关。可写性控制着对值特性的修改。可配置性控制着对其他特性(包括属性是否可以删除)的修改。然而规则远不止这么简单,例如,如果属性是可配置的话,则可以修改不可写属性的值。同样,如果属性是不可配置的,仍然可以将可写属性修改为不可写属性。下面是完整规则,任何对Object.defineProperty()
或Object.defineProperties()
违反规则的使用都会抛出类型错误异常
如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性
如果属性是不可配置的,则不能修改它的可配置性和可枚举性
如果存取器属性是不可配置的,则不能修改其
getter
和setter
方法,也不能将它转换为数据属性如果数据属性是不可配置的,则不能将它转换为存取器属性
如果数据属性是不可配置的,则不能将它的可写性从
false
修改为true
,但可以从true
修改为false
如果数据属性是不可配置且不可写的,则不能修改它的值。然而可配置但不可写属性的值是可以修改的(实际上是先将它标记为可写的,然后修改它的值,最后转换为不可写的)。
上文中的extend()
函数,只是简单的复制属性名和值,没有复制属性的特性,而且也没有复制存取器属性的getter
和setter
方法,只是将它们简单的转换为静态的数据属性。下面给出改进的extend()
,它使用Object.getOwnPropertyDescriptor()
和Object.defineProperty()
对属性的所有特性进行复制。新的extend()
作为不可枚举属性添加到Object.prototype
中,因此它是Object
上定义的方法,而不是一个独立的函数。
1 | /* |
1. getter和setter的老式API
通过对象直接量语法给新对象定义存取器属性时,不能通过查询属性的方式获取getter
和setter
方法,或给已有的对象添加新的存取器属性。在ES5中,可以通过Object.getOwnPropertyDescriptor()
和Object.defineProperty()
来完成这些工作。
在ES5标准被采纳前,大多数Javascript的实现已经可以支持对象直接量语法中的get
和set
写法,这些实现提供了非标准的老式API用来查询和设置getter
和setter
,这些API由4个方法组成,所有对象都拥有这些方法。
__lookupGetter__()
和__lookupSetter__()
用以返回一个命名属性的getter
和setter
方法__defineGetter__()
和__defineSetter__()
用以定义getter
和setter
,这两个函数的第一个参数是属性名字,第二个参数是getter
和setter
方法。这四个方法都是以两条下划线作前缀,两条下划线作后缀,以表明它们是非标准的方法。
八、对象的三个属性
每一个对象都有与之相关的原型(prototype
)、类(class
)和可扩展性(extensible attribute
)
1. 原型属性
对象的原型属性是用来继承属性的,原型属性是在实例对象创建之初就设置好的。在ES5中,将对象作为参数传入Object.getPrototypeOf()
可以查询它的原型。在ES3中,没有与之等价的函数,但经常使用表达式o.constructor.prototype
来检测一个对象的原型。通过new
表达式创建的对象,通常继承一个constructor
属性,这个属性指代创建这个对象的构造函数。注意,通过对象直接量或Object.create()
创建的对象包含一个名为constructor
的属性,这个属性指代Object()
构造函数。因此,constructor.prototype
才是对象直接量的真正的原型,但对于通过Object.create()
创建的对象则往往不是这样。
要想检测一个对象是否是另一个对象的原型(或处于原型链中),可以使用isPrototypeOf()
方法
1 | var p = {x: 1}; |
需要注意的是,isPrototypeOf()
函数实现的功能和instanceof
运算符非常类似。
Mozilla实现的JavaScript对外暴露了一个专门命名为
__proto__
的属性,用以直接查询/设置对象的原型。但并不推荐使用__proto__
,因为尽管Safari和Chrome的当前版本都支持它,但IE和Opera并为实现。实现了ES5的Firefox支持__proto__
,但对修改不可扩展对象的原型做了限制。
2. 类属性
对象的类属性(class attribute
)是一个字符串,用以表示对象的类型信息。ES3和ES5都未提供设置这个属性的方法,并只有一种简介的方法可以查询它。默认的toString()
方法(继承自Object.prototype
)返回了如下这种格式的字符串[object class]
。
因此,想要获得对象的类,可以调用对象的toString()
方法,然后提取已返回字符串中的字符。不过让人感觉棘手的是,很多对象继承的toString()
方法重写了,为了能调用正确的toString()
版本,必须间接地调用Function.call()
方法
1 | function classof(o) { |
classof()
函数可以传入任何类型的参数。数字、字符串和布尔值可以直接调用toString()
方法,就和对象调用toString()
方法一样,并且这个函数包含了对null
和undefined
的特殊处理(ES5中不需要进行特殊处理)。通过内置构造函数(比如Array
和Date
)创建的对象包含“类属性”(class attribute
),它与构造函数名称相匹配。宿主对象也包含有意义的“类属性”,但这和具体的JavaScript实现有关。
对于自定义的类来说,没办法通过类属性来区分对象的类
1 | classof(null) // => 'Null' |
3. 可扩展性
对象的可扩展性用以表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显示可扩展的,宿主对象的可扩展性是由JavaScript引擎定义的。在ES5中,所有的内置对象和自定义对象都是可扩展的,除非将它们转换为不可扩展的,同样,宿主对象的可扩展性也是由实现ES5的JavaScript引擎定义的。
ES5定义了用来查询和设置对象可扩展性的函数,通过将对象传入Object.isExtensible()
,来判断该对象是否是可扩展的。如果想将对象转换为不可扩展的,需要调用Object.preventExtensions()
,将待转换的对象作为参数穿进去。注意,一旦将对象转换为不可扩展的,就无法再将其转换回可扩展的了。同样需要注意的是,preventExtensions()
只影响到对象本身的可扩展性。如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。
可扩展属性的目的是将对象“锁定”,以避免外界的干扰。对象的可扩展性通常和属性的可配置性与可写性配合使用,ES5定义的一些函数可以更方便的设置多种属性。
Object.seal()
和Object.preventExtensions()
类似,除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。也就是说,不能给这个对象添加新属性,而且它已有的属性也不能删除或配置,不过它已有的科协属性依然可以设置。对于那些已经封闭(sealed
)起来的对象是不能解封的。可以使用Object.isSealed()
来检测对象是否封闭。
Object.freeze()
将更加严格地锁定对象——“冻结”(frozen
)。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它自有的所有数据属性设置为只读(如果对象的存取器属性具有setter
方法,存取器属性将不受影响,人可以通过给属性赋值调用它们)。使用Object.isFrozen()
来检测对象是否冻结。
Object.preventExtensions()
、Object.seal()
、Object.freeze()
都返回传入的对象,也就是说,可以通过函数嵌套的方式调用它们
1 | // 创建一个封闭对象,包括一个冻结的原型和一个不可枚举的属性 |
九、序列化对象
对象序列化(serialization
)是指将对象的状态转换为字符串,也可将字符串还原为对象。ES5提供了内置函数JSON.stringfigy()
和JSON.parse()
用来序列化和还原JavaScript对象。这些方法都是用JSON作为数据交换格式。
JSON的语法是JavaScript语法的子集,它并不能表示JavaScript离的所有值,支持对象、数组、字符串、无穷大数字、true
、false
和null
,并且它们可以序列化和还原。NaN
、Infinity
和-Infinity
序列化的结果是null
,日期对象序列化的结果是ISO格式的日期字符串(参照Date.toJson()
函数),但JSON.parse()
依然保留它们的字符串形态,而不会将它们还原为原始日期对象。函数、RegExp
、Error
对象和undefined
值不能序列化和还原。
JSON.stringfy()
只能序列化对象可枚举的自有属性
对于一个不能序列化的属性来说,在序列化后的输出字符串中会将这个属性省略掉。JSON.stringify()
和JSON.parse()
都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。
十、对象方法
所有的JavaScript对象都从Object.prototype
继承属性(除了那些不通过原型显示创建的对象)。这些继承属性主要是方法,我们已经讨论过hasOwnProperty()
、propertyIsEnumerable()
、isPrototypeOf()
三个方法,以及在Object构造函数里定义的静态函数Object.create()
和Object.getPrototypeOf()
等。本节将对定义在Object.prototype
里的对象方法展开讲解,这些方法非常好用而且使用广泛,但一些特定的类会重写这些方法。
1. toString()方法
toString()
方法没有参数,它返回一个表示调用这个方法的对象值的字符串。在需要将对象转换为字符串的时候,JavaScript都会调用这个方法。比如,当使用“+”运算符连接一个字符串和一个对象时或者在希望使用字符串的方法中使用了对象时都会调用toString()
。
默认的toString()
方法的返回值带有的信息量很少。
1 | var s = {x: 1, y: 1}.toString(); // "[object Object]" |
由于默认的toString()
方法并不会输出很多有用的信息,因此很多类都带有自定义的toString()
。例如,当数组转换为字符串的时候,结果实一个数组元素列表,只是每个元素都换成了字符串,再比如,当函数转换为字符串的时候,得到函数的源代码。
2. toLocaleString()方法
除了基本的toString()
方法之外,对象都包含toLocaleString()
方法,这个方法返回一个表示这个对象的本地化字符串。Object
中默认的toLocaleString()
方法并不做任何本地化自身的操作,它仅调用toString()
方法并返回对应值。Date
和Number
类对toLocaleString()
方法做了定制,可以用它对数字、日期和时间做本地化的转换。Array
类的toLocaleString()
方法和toString()
方法很像,唯一的不同是每个数组元素会调用toLocaleString()
方法转换为字符串,而不是调用各自的toString()
方法。
3. toJSON()方法
Object.prototype
实际上没有定义toJSON()
方法,但对于需要执行序列化的对象来说,JSON.stringify()
方法会调用toJSON()
方法。如果在待序列化的对象中存在这个方法,则调用它,返回值即是序列化的结果,而不是原始的对象。
4. valueOf()方法
valueOf()
方法和toString()
方法非常类似,但往往当JavaScript需要将对象转换为某种原始值而非字符串的时候才会调用它,尤其是转换为数字的时候。如果在需要使用原始值的上下文中使用了对象,JavaScript就会自动调用这个方法。默认的valueOf()
方法不足为奇,但有些内置类自定义了valueOf()
方法(比如Date.valueOf()
)。