对象是一种复合值,它是一个属性的无序集合,并允许我们按属性存储和获取值,对象的每个属性都有名字和值,属性名通常是字符串,因此对象把字符串映射为值。
对象简介

对象汇聚多个值(原始值或其他对象),是一个无序的集合(散列、散列表、字典、关联数组),我们可以通过属性名进行存储和获取值。不过,对象还可以从其他对象继承属性,这个其他对象称之为“原型”。

对象是动态的,即可以动态添加和删除属性,对象是按照引用操作而不是按值操作的,如果变量x指向一个对象,则let y = x执行后,变量y保存的是同一个对象的引用,而不是该对象的副本。请阅读[JS]对象的引用与拷贝

创建对象

对象可以通过字面量、new关键字和Object.create()函数来创建。

字面量

创建对象最简单的方式是在JS代码中直接包含对象字面量。

let empty = {}
let number = { x: 0, y: 0}
let book = {
    'main title': 'JavaScript',
    author: {
        name: 'David'
    }
}

new关键字

new关键字用于创建和初始化一个新对象,new关键字后面必须使用构造函数,目的是初始化新创建的对象。

let a = new Array()
let o = new Object()

Object.create()

Object.create()函数用于创建一个新对象,使用第一个参数作为新对象的原型:

let o = Object.create({ x: 1, y: 2 })

传入Object.prototype创建空对象:

let o = Object.create(Object.prototype)

传入null创建一个没有原型的对象,它不会继承任何东西。

属性

属性访问表达式有两种,一个是.property,而另一个是['property'],通过该表达式可以为对象创建、设置、获取属性:

book.edition = 7 // 创建一个edition属性
book['main title'] = 'ECMAScript' // 修改main title

关联数组

在JS中可以为任意对象创建任意数量的属性,因为JS的对象是关联数组(或散列、映射或字典),所以可以在程序运行期间修改和创建。

属性访问表达式['property']是以字符串作为索引的数组,字符串是动态的,可以在运行时修改;属性访问表达式.property是以标识符来访问属性的,标识符是静态的,必须硬编译到程序中。

利用.property不能够动态访问属性,反而会报错:

[JS]对象_字面量

而利用['property']能够动态访问属性:

for (let i = 0; i < 4; i++) {
    o[`hobby${i}`]
}

对象继承

对象有可能从它的原型对象继承一组属性,但会有一组自己的属性,叫做“自有属性”。

假设要从对象c中查询属性x,若没有该自有属性,则查询对象c的原型对象b是否拥有属性x,若对象b也没有属性x,则查询对象b的原型对象a是否拥有属性x。该过程会一直持续,直到找到属性x或者查询到一个原型为null的对象。

[JS]对象_字符串_02

let a = {}
a.x = 2
let b = Object.create(a)
b.y = 2
let c = Object.create(b)
c.z = 2
console.log(c.x) // 2

属性访问错误

属性访问表达式并不总是会返回或设置值,查询不存在的属性不是错误,如果在本次查询的对象的自有属性和继承属性中没有找到属性,则属性访问表达式的求值结果为undefined。列如,o对象有一个sub-title属性,没有subtitle属性:

o.subtitle // => undefined

若继续查询下去,属性访问表达式的左侧是null或undefined,则属性访问表达式会发生TypeError错误:

o.subtitle.length // => TypeError

因此,要确保o对象和o.subtitle是有定义的,可以通过?.条件是属性访问表达式防止TypeError错误,请阅读条件式属性访问

o?.subtitle?.length // => undefined

在实际开发中,比如在Vue的mounted生命周期函数请求异步数据通常会遇到Error in render: "TypeError: Cannot read property '' of undefined"

删除属性

delete操作符用于从对象中移除属性,它唯一的操作数是一个数学访问表达式,delete删除的不是属性的值,而是操作属性本身。

delete o.subtitle // 删除o对象的subtitle属性
delete o['subtitle']

delete操作符只删除自有属性,不删除继承属性。如果delete操作成功或没影响(如删除不存在的属性),则delete返回true,对非属性访问表达式使用delete,同样也会返回true。

let o = { x: 1, y: 2 }
delete o.x // true,删除属性x
delete o.y // true,什么也不做,因为属性y不存在
delete o.toString // true,什么也不做,因为toString不是自有属性

测试属性

实际开发中经常需要测试这组属性的成员关系,即检查对象是否有一个给定名字的属性。为此,可以使用in操作符,或者hasOwnProperty()、propertyIsEnumerable()方法,或者直接查询相应属性。

in操作符

in操作符要求左边是一个属性名,右边是一个对象,如果对象有包含相应名字的自由属性或继承属性,将返回true:

let o = { x: 1 }
'x' in o // true,o有自有属性x
'y' in o // false,o没有属性y
'toString' in o // true,o继承了Object的toString属性

hasOwnProperty()

hasOwnProperty()方法用于测试对象的属性是否为自有属性,但不能测试继承的属性:

let o = { x: 1 }
o.hasOwnProperty('x') // true,o有自有属性x
o.hasOwnProperty('y') // false,o没有属性y
o.hasOwnProperty('toString') // false,toString是继承属性

propertyIsEnumerable()

propertyIsEnumerable()方法用于测试对象的属性是否可枚举,方法参数接收对象的属性,该属性是自有属性且enumerable为true。某些内置属性是不可枚举的,常规创建的属性都是可枚举的,除非使它们限制为不可枚举:

let o = { x: 1 }
o.propertyIsEnumerable('x') // true,o有自有属性x
o.propertyIsEnumerable('toString') // false,toString不是自有属性
Object.prototype.propertyIsEnumerable('toString') // false,toString不可枚举

!==

除了使用上面几种操作符以外,还可以使用简单的!==测试属性是否存在于对象中:

let o = { x: 1 }
o.x !== undefined // true,o有自有属性x
o.y !== undefined // false,o没有属性y
o.toString !== undefined // true,o继承了Object的toString属性

!==不能区分undefined的属性,但in可以区分不存在的属性和存在但被设置为undefined的属性。

枚举属性

for/in

for/in循环对指定对象的每个可枚举属性(包括自有属性或继承属性)

let o = { x: 1, y: 2, z: 3 }
Object.defineProperty(o, 'v', { // 不可枚举属性v
    value: 'notEnumerable',
    writable: true,
    enumerable: false,
    configurable: true
})
for (let p in o) {
  console.log(p) // x y z ,v属性为不可枚举属性
}

Object.keys()

Object.keys()返回对象可枚举自有属性名的数组,不包含不可枚举属性、继承属性:

let a = {}
a['x'] = 'successfully'
let b = Object.create(a)
b['z'] = 'hello'
b['w'] = 'world'
Object.defineProperty(b, 'v', { // 不可枚举属性v
    value: 'notEnumerable',
    writable: true,
    enumerable: false,
    configurable: true
})
let keys = Object.keys(b) // ['z', 'w'] 可枚举属性且自有属性名的数组

Object.getOwnPropertyNames()

Object.getOwnPropertyNames()返回对象不可枚举和可枚举自有属性名的数组:

let a = {}
a['x'] = 'successfully'
let b = Object.create(a)
b['z'] = 'hello'
b['w'] = 'world'
Object.defineProperty(b, 'v', { // 不可枚举属性v
    value: 'notEnumerable',
    writable: true,
    enumerable: false,
    configurable: true
})
let keys = Object.getOwnPropertyNames(b) // ['z', 'w', 'v'] 可枚举的和不可枚举的属性名数组

Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols()返回对象符号属性:

let b = Object.create(a)
b['z'] = 'hello'
b['w'] = 'world'
b[Symbol('symbol')] = 'symbol'
let keys = Object.getOwnPropertySymbols(b) // [Symbol(symbol)]

Reflect.ownKeys()

Reflect.ownKeys()返回所有可枚举和不可枚举,以及字符串属性和符号属性:

let a = {}
a['x'] = 'successfully'
let b = Object.create(a)
b['z'] = 'hello'
b['w'] = 'world'
b[Symbol('symbol')] = 'symbol'
Object.defineProperty(b, 'v', { // 不可枚举属性v
    value: 'notEnumerable',
    writable: true,
    enumerable: false,
    configurable: true
})
let keys = Reflect.ownKeys(b) // ["z", "w", "v", Symbol(symbol)] 可枚举和不可枚举属性,字符串属性、符号属性
扩展对象

请阅读[JS]对象的引用与拷贝

对象序列化

对象序列化是把对象的状态转换为字符串的过程,之后可以从中恢复对象的状态。函数JSON.stringify()和JSON.parse()用于序列化和恢复JS对象。这两个函数使用JSON数据交换格式。JSON表示JavaScript Object Notation(JavaScript对象表示语法),其语法与JavaScript对象和数组字面量非常相似:

let o = { x: 1, y: { z: [false, null, ''] } }
let s = JSON.stringify(o) // {"x":1,"y":{"z":[false,null,""]}}
let p = JSON.parse(s)

JSON语法是JS语法的子集,可以序列化和恢复的值包括对象、数组、字符串、布尔、null。

对象方法

所有JS对象(除了显示创建为没有原型的)都从Object.prototype继承属性,如hasOwnProperty()、propertyIsEnumerable()等等。

toString()

默认的toString()只会得到对象的类型:

let s = { x: 1 }.toString() // [object Object]

一般会重新定义自己的toString()方法。

toJSON()

Object.prototype实际上并未定义toJSON()方法,但JSON.stringify()方法会从要序列化的对象上寻找toJSON()方法。Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。

let o = {
  x: 1,
  y: 2,
  toString: function() { return `(${this.x}, ${this.y})` }
  toJSON: function() { return this.toString() }
}

JSON.stringify([point]) // '["(1, 2)"]'
对象字面量扩展语法

计算的属性名

有时候,我们需要创建一个具有特定属性的对象,但该属性的名字不是编译时可以直接写在源代码中的常量。相反,你需要的这个属性名保存在一个变量里,或者是调用的某个函数的返回值。不能对这种属性使用基本对象字面量,为此需要先创建一个对象然后再为它添加想要的属性:

const PROPERTY_NAME = 'computed1'

function computePropertyName() {
    return 'computed2'
}

let o = {
    [PROPERTY_NAME]: 'hello world'
    [computePropertyName()]: 'hello world'
}

let keys = Object.keys(o)
console.log(`key => ${keys[0]}`, `value => ${o['computed1']}`) // key => computed1, value => hello world
console.log(`key => ${keys[1]}`, `value => ${o['computed2']}`) // key => computed2, value => hello world

符号作为属性名

在ES6只有,属性名可以是字符串或符号,如果把符号赋值给一个变量或常量,那么可以使用计算属性语法将该符号作为属性名:

const extension = Symbol('mySymbol')

let o = {
  [extension]: { /* object ... */ }
}

o[extension].x = 0

符号除了用作属性名之外,不能用它们做任何事情。不过每个符号都与其他符号不同,这意味着符号非常使用用于创建唯一属性名。创建符号需要调用Symbol()工厂函数(符号不是对象,而是原始值,因此Symbol()不是构造函数,不能用new调用),使用相同符号创建的两个符号依旧是不同的符号。

符号是为了JavaScript对象定义安全的扩展机制,如果你从第三方代码得到一个对象,然后需要为该对象添加一些自己的属性,但又不希望新属性与该对象原有的任何属性冲突,那就可以使用符号作为属性名。

扩展操作符

在ES2018及以后,可以在对象字面量中使用“扩展操作符”——...把已有的对象属性复制到新对象中:

let p = { x: 0, y: 0 }
let d = { width: 100, height: 75 }
let o = { ...p, ...d }
o.x + o.y + o.width + o.height // 175

...操作符把p对象和d对象的属性扩展到了o对象字面量中。实际上,它仅在对象字面量中有效的一种特殊语法,即复制已有的对象属性到新对象中。

扩展操作符只扩展对象的自由属性,不扩展任何继承属性:

let o = Object.create({x: 1})
let p = { ...o }
p.x // undefined

若对象有n个属性,把这个属性扩展到另一个对象可能是O(n)操作。这意味着,如果循环或递归函数中向一个大对象不断追加属性,可能是o(n²)。随着n越来越大,扩展性能就越低。

简写方法

把函数定义为对象属性时,我们称函数为方法。在ES6以前,需要像定义对象的其他属性一样,通过函数定义表达式在对象字面量中定义一个方法:

let square = {
  area: function() { return this.side * this.side },
  side: 10
}

square.area() // 100

在ES6中,对象字面量语法允许省略function关键字和冒号的简写方法:

let square = {
  area() { return this.side * this.side },
  side: 10
}

square.area() // 100

在使用这种简写方法时,属性名可以是对象字面量允许的任何形式,也可以使用字符串字面量和计算的属性名,包括符号属性名:

const METHOD_NAME = 'm'
const symbol = Symbol()

let wm = {
  'method with space'(x) { return x + 1 },
  [METHOD_NAME](x) { return x + 2 },
  [symbol](x) { return x + 3 }
}

wm['method with space'](1) // 2
wm[METHOD_NAME](1) // 3
wm[symbol](1) // 4

访问器属性

JS还支持为对象定义访问器属性,这种属性不是一个值,而是一个或两个访问器方法:getter和setter。

当获取一个访问器属性的值时,JS会调用获取方法。这个方法的返回值就是属性访问表达式的值。当设置一个访问器属性的值时,JS会调用设置方法,传入赋值语句右边的值。

当只有一个设置方法时,那它就是只写属性,读取这种属性始终会得到undefined:

let o = {
    _x: 0,
    set x(x) {
        this._x = x
    }
}

o.x = 10 // undefined

当只有一个获取方法,那它就是只读属性:

let o = {
    _x: 0,
    get x(x) {
        return this._x = x
    }
}

o.x // 0

使用访问器属性的其他场景还有写入属性时进行合理性检测,以及每次读取属性时返回不同的值:

const serialnum = {
  _n: 0,
  get next() { return this._n++ },
  set next(n) { 
    if (n > this._n) this._n = n
    else throw new Error('serial number can only be set to a larger value')
  }
}

serialnum.next = 10 // 初始化值
serialnum.next // 10
serialnum.next // 11,每次读取的值不同