对象汇聚多个值(原始值或其他对象),是一个无序的集合(散列、散列表、字典、关联数组),我们可以通过属性名进行存储和获取值。不过,对象还可以从其他对象继承属性,这个其他对象称之为“原型”。
对象是动态的,即可以动态添加和删除属性,对象是按照引用操作而不是按值操作的,如果变量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
不能够动态访问属性,反而会报错:
而利用['property']
能够动态访问属性:
for (let i = 0; i < 4; i++) {
o[`hobby${i}`]
}
对象继承
对象有可能从它的原型对象继承一组属性,但会有一组自己的属性,叫做“自有属性”。
假设要从对象c中查询属性x,若没有该自有属性,则查询对象c的原型对象b是否拥有属性x,若对象b也没有属性x,则查询对象b的原型对象a是否拥有属性x。该过程会一直持续,直到找到属性x或者查询到一个原型为null的对象。
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,每次读取的值不同