# 对象

对象是 javascript 的基本数值类型。对象是一种复合值：它将很多值(原始值或者其他对象)聚合在一起，可通过名字访问这些值。对象也可看做是属性的无序集合，每个属性都是一个名/值对。属性名是字符串，因此我们可以把对象看成是从字符串到值的映射。不过对象不仅仅是字符串到值的映射，除了可以保持自有的属性，javascript 对象还可以从一个称为原型的对象继承属性，对象的方法通常是继承的属性。这种“原型继承”是 javascript 的核心特征。

除了字符串、数字、true、false、null 和 undefined 之外，javascript 中的值都是对象。尽管字符串、数字和布尔值不是对象，但它们的行为和不可变对象非常类似。

对象是可变的，我们通过引用而非值来操作对象。如果变量 `x` 是指向一个对象的引用，那么执行代码 `var y = x`；变量 `y` 也是指向同一个对象的引用，而非这个对象的副本。通过变量 `y` 修改这个对象亦会对变量 `x` 造成影响。

除了名字和之外，每个属性还有一些与之相关的值，称为“属性特性”：

1. 可写，表明是否可以设置该属性的值。
2. 可枚举，表明是否可以通过 `for/in` 循环返回该属性。
3. 可配置，表明是否可以删除或修改该属性。

除了包含属性之外，每个对象还拥有三个相关的对象特性：

1. 对象的原型指向另一个对象，本对象的属性继承自它的原型对象。
2. 对象的类是一个标识对象类型的字符串。
3. 对象的拓展标记指明了是否可以向该对象添加新属性。

最后，可用下面这些术语来对三类 javascript 对象和两类属性作区分：

1. 内置对象是有 ECMAScript 规范定义的对象或类。例如，数组、函数、日期和正则表达式都是内置对象。
2. 宿主对象是由 javascript 解释器所嵌入的宿主环境定义的。客户端 javascript 中表示网页结构的 `HTMLElement` 对象均是宿主对象。既然宿主环境定义的方法可以当成普通的 javascript 函数对象，那么宿主对象也可以当成内置对象。
3. 自定义对象是由运行中的 javascript 代码创键的对象。
4. 自由属性是直接在对象中定义的属性。
5. 继承属性是在对象原型对象中定义的属性。

### 创键对象

#### 对象直接量

对象属性名可以是 javascript 标识符也可以是字符串直接量，包括空字符串。例如：

```javascript
var book = {
    "main-title": "XXX",  //属性名字里有空格，必须用字符串表示
    "sub-title":"XXX",  //属性名字里有连字符，必须用字符串表示
     "for": "XXX"         //“for”是保留字，因此必须用引号
     author: {            //这个属性的值是一个对象
       firstname: "XXX",  //注意，这里的属性名都没有引号
       surname: "XXX"
     }
};
```

在 ECMAScript5 中，对象直接量中的最后一个属性后的逗号将忽略，且在 ECMAScript 3 的大部分实现中也可以忽略这个逗号，但在 IE 中则报错。

#### Object.create()

`Object.create()`是一个静态函数，而不是提供给某个对象调用的方法。例如：

```javascript
var 01 = Object.create({x:1,y:2});   // 01继承了属性x和y
```

可通过传入 `null` 来创建一个没有原型的新对象，但通过这种方式创建的对象不会继承任何东西，甚至不会继承基础方法，比如 `toString()`。如果想创建一个普通的空对象，需要传入 `Object.prototype`。

### 属性查询设置

在 ES3 中，点运算符后的标识符不能是保留字，如果一个对象的属性名是保留字，则必须使用括号的形式访问它们，比如 `o["for"]`,ES 对此放宽了限制(包括 ES3 的某些实现),可以在点运算后直接使用保留字。

#### 继承

属性赋值操作首先会检查原型链，以此判定是否允许赋值操作。例如，如果 `o` 继承自一个只读属性 `x`，那么赋值操作是不允许的。如果允许赋值操作，它总是在原始值对象上创建属性或对已有的属性赋值，而不会去修改原型链。在 javascript 中，只有查询属性时才会体会到继承的存在，设置属性与继承无关。

属性赋值要么失败，要么创建一个属性，要么在原始对象中设置属性，但有一个例外，如果 `o` 继承自属性 `x`，而这个属性是一个 `setter` 方法的 `accessor` 属性，那么这时将调用 `setter` 方法而不是给 `o` 创建一个属性 `x`。需要注意的是，`setter` 方法是由对象 `o` 调用的，而不是定义这个属性的原型对象调用的，因此如果 `setter` 方法定义在任意属性，这个操作只是针对 `o` 本身，并不会修改原型链。

#### 属性访问错误

查询一个不存在的属性并不会报错，如果在对象 `o` 自身的属性或继承的属性中均未找到属性 `x`，属性访问表达式 `o.x` 返回 `undefined`。但是如果对象不存在，那么试图查询这个不存在的对象属性就会报错。例如：

```javascript
// 抛出一个类型错误异常，undefined和null没有属性
var z = o.x.y
```

尽管属性赋值成功或失败的规律看起来很简单。但描述清楚不容易。在这些场景下给对象 `o` 设置属性 `p` 会失败：

1. `o` 中的属性 `p` 是只读的。
2. `o` 中的属性是 `p` 继承属性，且是只读的：不能通过同名自由属性覆盖只读的继承属性。
3. `o` 中不存在的自由属性 `p`，`o` 没有使用 `setter` 方法继承属性 `p`，并且 `o` 的可拓展性是 `false`。如果 `o` 中不存在 `p`，而且没有 `setter` 方法可调用，则 `p` 一定会添加到 `o` 中。但如果 `o` 不是可拓展的，那么在 `o` 中不能定义新属性。

### 删除属性

`delete` 只是断开属性和宿主对象的联系，而不会去操作属性中的属性：

```javascript
a = { p: { x: 1 } }
b = a.p
delete a.p
```

执行上段代码后，`b.x` 的值依然是 `1`。由于已经删除的属性的引用依然存在，因此在 javascript 的某些实现中，可能因为这种不严谨的代码而造成内存泄漏。所以在销毁对象时，要遍历属性中的属性，依次删除。

当 `delete` 删除成功或没有任何副作用时；它返回 `true`，如果 `delete` 后不是一个属性表达式，`delete` 同样返回 `true`：

```javascript
o = { x: 1 } // o有一个属性x，并继承属性toString
delete o.x // 删除x，返回true
delete o.x // 什么也没做，x不存在，返回true
delete o.toString // 什么也没做，toString是继承来的，返回true
delete 1 // 无意义，返回true
```

`delete` 不能删除那些可配置性为 `false` 的属性：

```javascript
delete Object.prototype // 不能删除，属性是不可配置的
var x = 1 // 声明一个全局变量
delete this.x // 不能删除这个属性
function f() {} // 声明一个去全局函数
delete this.f // 也不能删除全局变量
```

当在非严格模式中删除全局对象的可配置属性是，可以省略对全局对象的引用，直接在 `delete` 操作符后跟随要要删除的属性名即可：

```javascript
this.x = 1 // 创建一个可配置的全局变量 (没有使用var)
delete x // 将其删除
```

然而在严格模式中，`delete` 后跟随一个非法的操作数，则会报一个语法错误，因此必须显示指定对象及其属性：

```javascript
delete x // 在严格模式下语法报错
delete this.x // 正常工作
```

### 检测属性

判断某个属性是否存在于某个对象中。可以通过 `in` 运算、`hasOwnPreperty()`和 `propertyIsEnumerable()`方法来完成这个工作。

`in` 运算符的左侧是属性名，右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回 `true`：

```javascript
var o = { x: 1 }
'x' in o // true："x"是o的属性
'y' in o // false："y"不是o的属性
'toString' in o // true：o继承toString属性
```

对象的 `hasOwnProperty()`方法用来检测给定的名字是否是对象的自有属性。

```javascript
var o = { x: 1 }
o.hasOwnproperty('x') // true
o.hasOwnproperty('y') // false
o.hasOwnproperty('toString') // false：继承的属性
```

`propertyIsEnumerable()`是 `hasOwnProperty()`的增强版，只有检测到是自有属性且这个属性是可枚举性为 `true` 时它才返回 `true`。

### 枚举属性

除了 `for/in` 循环之外，ES5 定义了两个用以枚举属性名称的函数。第一个是 `Object.keys()`,它返回一个数组，这个数组有对象中可枚举的自有属性的名称组成。

ES5 中第二个枚举属性的函数是 `Object.getOwnPropertyNames()`，它和 `Object.keys()`类似，只是它返回对象的所有自有属性的名称，而不仅仅是可枚举的属性。

### getter 和 setter

由 `getter` 和 `setter` 定义的属性称做“存取器属性”，它不同于“数据属性”，数据属性只有一个简单的值。存取器属性不具有可写性。如果属性同时具有 `getter` 和 `setter` 方法，那么它是一个读/写属性。如果它只有 `getter` 方法，那么它是一个只读属性。如果它只有 `setter` 方法，那么它是一个只写属性，读取只写属性总是返回 `undefined`。

定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法：

```javascript
var p = {
  //x是普通的可读写的数据属性
  x: 1.0,

  // theta是只读存取器
  get theta() {
    return this.x
  }
}
```

这里 javascript 把这些函数当做对象的方法来调用，也就是说，在函数体内的 `this` 指向表示这个点的对象，因此，r 属性的 `getter` 方法可以通过 `this.x` 引用 `x` 的属性。

和数据属性一样，存取器属性是可以继承的，因此可以将上述代码中的对象 `p` 当做另一个对象的原型。可以给新对象定义它的 `x` 属性。

很多场景可以用到存取器属性，比如智能检测属性的写入值以及在每次属性读取时返回不同的值：

```javascript
// 这个对象产生严格自增的序列号
var serialnum = {
    // 这个数据属性包含下一个序列号
    // $符号暗示这个属性是私有的
    $n: 0,
    // 返回当前值，然后自增
    get next() { return this.$n++ },
    // 给n设置新的值，但只有当它比当前值大时才设置成功
    set next(n) {
        if (n >= this.$n ) this.$n = n;
        else throw “序列号的值不能不能比当前值小”;
    }
};
```

### 属性的特性

除了包含名字和值之外，属性还包含一些标识它们可写、可枚举和可配置的特性。在 ES5 中查询和设置这些特性的 API。这些 API 对于库的开发者来说非常重要，因为：

1. 可以通过这些 API 给原型对象添加方法，并将它们设置成不可枚举的，这让它们看起来更像内置方法。
2. 可以通过这些 API 给对象定义不能修改或删除的属性，借此“锁定”这个对象。

为了实现属性特性的查询和设置操作，ES5 中定义了一个名为“属性描述符”的对象，这个对象代表了读取(get)、写入(set)、可枚举和可配置性。数据属性的描述符对象的属性有 `value`、`writable`、`enumerable` 和 `configurable`。存取器属性的描述符对象则用 `get` 属性和 `set` 属性代替 `value` 和 `writable`。其中 `writable`、`enumerable` 和 `configurable` 都是布尔值，当然，`get` 属性和 `set` 属性是函数值。

通过调用 `Object.getOwnPropertyDescriptor()`可以获得某个对象特定属性的属性描述符：

```javascript
Object.getOwnPropertyDescriptor({ x: 1 }, 'x')
// 对于继承属性和不存在的属性，返回undefined
Object.getOwnPropertyDescriptor({}, 'x')
Object.getOwnPropertyDescriptor({}, 'toString')
```

要想设置属性的特性，或者想让新建属性具有某种特性，则需要调用 `Object.definePeoperty()`,传入要修改的对象。要创建或修改的属性的名称以及属性描述符对象：

```javascript
var o = {} // 创建一个空对象
// 添加一个不可枚举的数据属性x，并赋值为1
object.defineProperty(o, 'x', {
  value: 1,
  writable: true,
  enumerable: false,
  configurable: true
})

// 现在对属性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 0
  }
})
o.x // => 0
```

如果要同时修改或创建多个属性，则需要使用 `Object.defineProperties()`。第一个参数是要修改的对象，第二个参数是一个映射表，它包含要新建或修改的属性的名称，以及它们的属性描述符，例如：

```javascript
var p = Object.defineProperties(
  {},
  {
    x: { value: 1, writable: true },
    r: {
      get function() {
        return 1
      },
      enumerable: true
    }
  }
)
```

对于那些不允许创建或修改的属性来说，如果用 `Object.defineProperty()`和 `Object.defineProperties()`对其操作就会抛出类型错误异常，比如，给一个不可扩展的对象新增属性。造成这些方法抛出类型错误异常的其他原因则和特性本身相关。下面是完整的规则，任何对 `Object.defineProperty()`或 `Object.defineProperties()`违反规则的使用都会抛出类型错误异常：

1. 如果对象是不可扩展的，则可以编辑已有的自有属性，但不能给它添加新属性。
2. 如果属性是不可配置的，则不能修改它的可配置性和可枚举性。
3. 如果存取器属性是不可配置的，则不能修改其 `getter` 和 `setter` 方法，也不能将它转换为数据属性。
4. 如果数据属性是不可配置的，则不能将它转换为存取器属性。
5. 如果数据属性是不可配置的，则不能将它的可写性从 `false` 修改为 `true`，但可以从 `true` 修改为 `false`。
6. 如果数据属性是不可配置且不可写的，则不能修改它的值。然而可配置但不可写属性的值是可以修改的，实际上是先将它标记为可写的，然后修改它的值，转后转换为不可写的。

### 对象三属性

#### 原型属性

在 ES5 中，将对象作为参数传入 `Object.getPrototypeOf()`可以查询它的原型。

要想检测一个对象是否是另一个对象的原型，请使用 `isPrototypeOf()`方法。例如，可以通过 `p.isPrototypeOf(o)`来检测 `p` 是否是 `o` 的原型：

```javascript
var p = { x: 1 } // 定义一个原型对象
var o = Object.create(p) // 使用这个原型创建一个对象
p.isPrototypeOf(o) // => true:o继承自p
Object.prototype.isPrototypeOf(o) // => true:p继续自Object.prototype
```

#### 类属性

对象的类属性是一个字符串，用以表示对象的类型信息。ES3 和 ES5 都未提供设置这个属性的方法，并只有一种间接的方法可以查询它。默认的 toString()方法返回了如下这种格式的字符串：

```javascript
[object class]
```

因此，要想多的对象的类，可以调用对象的 `toString()`方法，然后提取已返回字符串的第 `8` 个到倒数第二个位置之间的字符串。不过有很多的对象的 `toString()`方法重写了，为了能正确的调用 `toSring()`版本，必须间接的调用 `Function.call` 方法：

```javascript
function classof(o) {
  if (o === null) return 'Null'
  if (o === undefined) return 'Undefined'
  return Object.prototype.toString.call(o).slice(8, -1)
}
```

#### 可扩展性

对象的可扩展性用以表示是否可以给对象添加新属性。所有的内置对象和自定义的对象都是显示可扩展的，宿主对象的可扩展性是由 javascript 引擎定义的。在 ES5 中，所有的内置对象和自定义对象都是可扩展的，除非将它们转换为不可扩展的。

ES5 定义了用来查询和设置对象的扩展性的函数。通过将对象传入 `Object.isExtensible()`,来判断对象是否是可扩展的。如果想将对象转换为不可扩展的，需调用 `Object.preventExtensions()`,将待转换的对象作为参数传进去。`preventExtensions()`只影响对象本身的可扩展性。如果给一个不可扩展的对象的原型添加属性，这个不可扩展的对象同样会继承这些新属性。

`Object.seal()`除了能够将对象设置为不可扩展的，还可以将对象的所有自有属性都设置为不可配置的。也就是说，不能给这些对象添加属性，而且它已有的属性也不能删除或配置。可用 `Object.isSealed()`来检测对象是否封闭。

`Object.freeze()`将更严格的锁定对象 ---- “冻结”。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外，还可以将它自有的所有数据属性设置为只读(如果对象的存取器具有 `setter` 方法，将不受影响，仍可以通过给属性赋值调用它们)。使用 `Object.isFrozen()`检测对象是否冻结。

`Object.preventExtensions()`、`Object.seal()`和 `Object.freeze()`都将返回传入的对象，即可通过函数嵌套的方式调用它们：

```javascript
// 创建一个封闭对象，包括一个冻结的原型和一个不可枚举的属性
var o = Object.seal(
  Object.creat(Object.freeze({ x: 1 }), { y: { value: 2, writable: true } })
)
```

### 序列化对象

对象序列化是指将对象的状态转换为字符串，也可将字符串还原为对象。ES5 提供了内置的函数 `JSON.stringify()`和 `JSON.parse()`来序列化和还原 javascript 对象。这些方法都使用 `JSON` 作为数据交换格式，它的语法和 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的深拷贝
```

在 ES3 中引入[json2.js](http://json.org/json2.js),便可使用以上函数。

`JSON` 的语法是 javascript 语法的子集，它并不能表示 javascript 里的所有值。`NaN`、`Infinity` 和`-Infinity` 序列化的结果是 `null`。函数、`RegExp`、`Error` 对象和 `undefined` 值不能序列化和还原（在谷歌中`undefined`可序列化）。`JSOn.stringify()`只能序列化对象的可枚举自有属性。这两个函数都可以接收第二个可选参数(数组或函数)，通过传入要序列化或还原的属性列表来定制自定义的序列化或还原操作。

#### toJSON()方法

`Object.prototype` 实际上没有定义 `toJSON()`方法，但对于需要执行序列化的对象来说，`JSON.stringify()`方法会调用 `toJSON()`方法。如果在待序列化的对象中存在这个方法，则调用它，返回值即是序列化的结果，而不是原始的对象。
