????????对象是JavaScript最基本的数据类型,前几章我们已经多次看到它了。因为对JavaScript语言来说对象实在太重要了,所以理解对象的详细工作机制也非常重要,本章就来详尽地讲解对象。一开始我们先正式地介绍一下对象,接下来几节将结合实践讨论创建对象和查询、设置、删除、测试以及枚举对象的属性。在关注属性的几节之后,接着会讨论如何扩展、序列化对象,以及在对象上定义重要方法。本章最后一节比较长,主要讲解ES6和这门语言的新近版本新增的对象字面量语法。
????????对象是一种复合值,它汇聚多个值(原始值或其他对象)并允许我们按名字存储和获取这些值。对象是一个属性的无序集合,每个属性都有名字和值。属性名通常是字符串,因此可以说对象把字符串映射为值。这种字符串到值的映射曾经有很多种叫法,包括“散列”“散列表”“字典”或“关联数组”等熟悉的基本数据结构。不过,对象不仅仅是简单的字符串到值的映射。除了维持自己的属性之外,JavaScript对象也可以从其他对象继承属性,这个其他对象称为其“原型”。对象的方法通常是继承来的属性,而这种“原型式继承”也是JavaScript的主要特性。
????????JavaScript对象是动态的,即可以动态添加和删除属性。不过,可以用对象来模拟静态类型语言中的静态对象和“结构体”。对象也可以用于表示一组字符串(忽略字符串到值的映射中的值)。
????????在JavaScript中,任何不是字符串、数值、符号或true、false、null、undefined的值都是对象。即使字符串、数值和布尔值不是对象,它们的行为也类似不可修改的对象。
????????对象可以通过对象字面量、new关键字和Object.create()函数来创建。接下来分别介绍这几种技术。
????????创建对象最简单的方式是在JavaScript代码中直接包含对象字面量。对象字面量的最简单形式是包含在一对花括号中的一组逗号分隔的“名:值”对。属性名是JavaScript标识符或字符串字面量(允许空字符串)。属性值是任何JavaScript表达式,这个表达式的值(可以是原始值或对象值)会变成属性的值。下面看几个示例:
????????对象字面量最后一个属性后面的逗号是合法的,有些编程风格指南鼓励添加这些逗号,以便将来在对象字面量末尾再增加新属性时不会导致语法错误。
????????对象字面量是一个表达式,每次求值都会创建并初始化一个新的、不一样的对象。字面量每次被求值的时候,它的每个属性的值也会被求值。这意味着同一个对象字面量如果出现在循环体中,或出现在被重复调用的函数体内,可以创建很多新对象,且这些对象属性的值可能不同。
????????new操作符用于创建和初始化一个新对象。new关键字后面必须跟一个函数调用。以这种方式使用的函数被称为构造函数(constructor),目的是初始化新创建的对象。JavaScript为内置的类型提供了构造函数。例如:
????????在介绍第三种创建对象的技术之前,必须暂停一下,先介绍原型。几乎每个JavaScript对象都有另一个与之关联的对象。这另一个对象被称为原型(prototype),第一个对象从这个原型继承属性。
????????通过对象字面量创建的所有对象都有相同的原型对象,在JavaScript代码中可以通Object.prototype引用这个原型对象。使用new关键字和构造函数调用创建的对象,使用构造函数prototype属性的值作为它们的原型。换句话说,使用new Object()创建的对象继承自Object.prototype,与通过{}创建的对象一样。类似地,通过new Array()创建的对象以Array.prototype为原型,通过new Date()创建的对象以Date.prototype为原型。对于JavaScript初学者,这一块很容易迷惑。记住:几乎所有对象都有原型,但只有少数对象有prototype属性。正是这些有prototype属性的对象为所有其他对象定义了原型。
????????Object.prototype是为数不多的没有原型的对象,因为它不继承任何属性。其他原型对象都是常规对象,都有自己的原型。多数内置构造函数(和多数用户定义的构造函数)的原型都继承自Object.prototype。例如,Date.prototype从Object.prototype继承属性,因此通过new Date()创建的日期对象从Date.prototype和Object.prototype继承属性。这种原型对象链接起来的序列被称为原型链。
????????Object.create()用于创建一个新对象,使用其第一个参数作为新对象的原型:
????????传入null可以创建一个没有原型的新对象。不过,这样创建的新对象不会继承任何东西,连toString()这种基本方法都没有(意味着不能对该对象应用+操作符):
????????如果想创建一个普通的空对象(类似{}或new Object()返回的对象),传入Object.prototype:
????????能够以任意原型创建新对象是一种非常强大的技术,本章多处都会使用Object.create()(Object.create()还可接收可选的第二个参数,用于描述新对象的属性。
????????要获得一个属性的值,可以使用4.4节介绍的点(.)或方括号([])操作符。左边应该是一个表达式,其值为一个对象。如果使用点操作符,右边必须是一个命名属性的简单标识符。如果使用方括号,方括号中的值必须是一个表达式,其结果为包含目的属性名的字符串:
????????要创建或设置属性,与查询属性一样,可以使用点或方括号,只是要把它们放到赋值表达式的左边:
????????使用方括号时,我们说过其中的表达式必须求值为一个字符串。更准确的说法是,该表达式必须求值为一个字符串或一个可以转换为字符串或符号的值。
如前所述,下面两个JavaScript表达式的值相同:
????????第一种语法使用点和标识符,与在C或Java中访问结构体或对象的静态字段的语法类似。第二种语法使用方括号和字符串,看起来像访问数组,只不过是以字符串而非数值作为索引的数组。这种数组也被称为关联数组(或散列、映射、字典)。JavaScript对象是关联数组,本节解释为什么这一点很重要。
????????在通过方括号([])这种数组表示法访问对象属性时,属性名是通过字符串来表示的。字符串是一种JavaScript数组类型,因此可以在程序运行期间修改和创建。
????????JavaScript对象有一组“自有属性”,同时也从它们的原型对象继承一组属性。要理解这一点,必须更详细地分析属性存取。本节的示例将使用Object.create()函数以指定原型来创建对象。每次通过new创建一个类的实例,都会创建从某个原型对象继承属性的对象。
????????属性访问表达式并不总是会返回或设置值。本节解释查询或设置属性时可能出错的情况。
????????查询不存在的属性不是错误。如果在o的自有属性和继承属性中都没找到属性x,则属性访问表达式o.x的求值结果为undefined。例如,book对象有一个“sub-title”属性,没有“subtitle”属性:
????????然而,查询不存在对象的属性则是错误。因为null和undefined值没有属性,查询这两个值的属性是错误。继续前面的示例:
????????如果.的左边是null或undefined,则属性访问表达式会失败。因此在写类似book.author.surname这样的表达式时,要确保book和book.author是有定义的。以下是两种防止这类问题的写法:
????????delete操作符用于从对象中移除属性。它唯一的操作数应该是一个属性访问表达式。令人惊讶的是,delete并不操作属性的值,而是操作属性本身:
????????delete操作符只删除自有属性,不删除继承属性(要删除继承属性,必须从定义属性的原型对象上删除。这样做会影响继承该原型的所有对象)。
????????如果delete操作成功或没有影响(如删除不存在的属性),则delete表达式求值为true。对非属性访问表达式(无意义地)使用delete,同样也会求值为true:
????????delete不会删除configurable特性为false的属性。与通过变量声明或函数声明创建的全局对象的属性一样,某些内置对象的属性也是不可配置的。在严格模式下,尝试删除不可配置的属性会导致TypeError。在非严格模式下,delete直接求值为false:
????????在非严格模式下删除全局对象可配置的属性时,可以省略对全局对象的引用,只在delete操作符后面加上属性名:
????????在严格模式下,如果操作数是一个像x这样的非限定标识符,delete会抛出SyntaxError,即必须写出完整的属性访问表达式:
????????如前所述,所有JavaScript对象(除了那些显式创建为没有原型的)都从Object.prototype继承属性。这些继承的属性主要是方法,因为它们几乎无处不在,所以对JavaScript程序而言特别重要。例如,前面我们已经看到过hasOwnProperty()和propertyIsEnumerable()方法了(而且我们也介绍了几个定义在Object构造函数上的静态方法,例如Object.create()和Object.keys())。本节讲解Object.prototype上定义的几个通用方法,但这些方法很有可能被更特定的实现所取代。后面几节我们将展示在同一个对象上定义这些方法的示例。
????????toString()方法不接收参数,返回表示调用它的对象的值的字符串。每当需要把一个对象转换为字符串时,JavaScript就会调用该对象的这个方法。例如,在使用+操作符拼接一个字符串和一个对象时,或者把一个对象传入期望字符串参数的方法时。
????????默认的toString()方法并不能提供太多信息(但可以用于确定对象的类。例如,下面这行代码只会得到字符串“[object Object]”:
????????由于这个默认方法不会显示太有用的信息,很多类都会重新定义自己的toString()方法。例如,在把数组转换为字符串时,可以得到数组元素的一个列表,每个元素也都会转换为字符串。而把函数转换为字符串时,可以得到函数的源代码。可以像下面这样定义自己的toString()方法:
????????除了基本的toString()方法之外,对象也都有一个toLocaleString()方法。这个方法的用途是返回对象的本地化字符串表示。Object定义的默认toLocaleString()方法本身没有实现任何本地化,而是简单地调用toString()并返回该值。Date和Number类定义了自己的toLocaleString()方法,尝试根据本地惯例格式化数值、日期和时间。数组也定义了一个与toString()类似的toLocaleString()方法,只不过它会调用每个数组元素的toLocaleString()方法,而不是调用它们的toString()方法。对于前面的point对象,我们也可以如法炮制:
????????valueOf()方法与toString()方法很相似,但会在JavaScript需要把对象转换为某些非字符串原始值(通常是数值)时被调用。如果在需要原始值的上下文中使用了对象,JavaScript会自动调用这个对象的valueOf()方法。默认的valueOf()方法并没有做什么,因此一些内置类定义了自己的valueOf()方法。Date类定义的valueOf()方法可以将日期转换为数值,这样就让日期对象可以通过<和>操作符来进行比较。类似地,对于point对象,我们也可以定义一个返回原点与当前点之间距离的valueOf():
????????Object.prototype实际上并未定义toJSON()方法,但JSON.stringify()方法(参见6.8节)会从要序列化的对象上寻找toJSON()方法。如果要序列化的对象上存在这个方法,就会调用它,然后序列化该方法的返回值,而不是原始对象。Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。同样,我们也可以给point对象定义这个方法: