对象的定义
对象汇聚多个值(原始值或其他对象),是一个无序的集合(散列、散列表、字典、关联数组),我们可以通过属性名进行存储和获取值。不过,对象还可以从其他对象继承属性,这个其他对象称之为“原型”。
对象是动态的,即可以动态添加和删除属性,对象是按照引用操作而不是按值操作的,如果变量x指向一个对象,则
let y = x
执行后,变量y保存的是同一个对象的引用,而不是该对象的副本。请阅读[JS]对象的引用与拷贝。
对象字面量
创建对象最简单的方式是对象字面量。
let empty = {}let number = { x: 0, y: 0}
new 关键字
new 关键字用于创建和初始化一个新对象,new 关键字后面必须使用构造函数,目的是初始化新创建的对象。
let a = new Array()let o = new Object()
Object.create()
Object.create() 函数用于创建一个新对象,该对象的原型对象是参数提供的对象。请阅读[JS]原型对象和原型链。
let o = {x: 1,y: 2}let a = Object.create(o)a.x // 1
传入 null 创建一个没有原型对象的新对象。
属性
通过属性访问表达式可以创建对象、设置(或添加)属性、删除属性。
let o = { x: 1 }o.y = 2 // 添加属性o[\'x\'] = 3 // 修改属性
关联数组
JavaScript 对象本质是关联数组。关联数组是一种具有特殊索引方式的数组。不仅可以通过整数来索引它,还可以使用字符串或者其他类型的值(除了NULL)来索引它。
如图,这种普遍访问对象的方式对于字符串是不可行的:
关联数组访问属性类似于数组访问元素一样,这种属性访问表达式就支持字符串的索引方式:
for (let i = 0; i < 4; i++) {o[`hobby${i}`]}
对象继承
对象有可能从它的原型对象中继承一组属性,但会有一组自己的属性,叫做“自有属性”。
let a = {}a.x = 2let b = Object.create(a)b.y = 2let c = Object.create(b)c.z = 2console.log(c.x) // 2
访问 c.x,若对象 c 没有自有属性 x,则查询它的原型对象 b 是否拥有属性 x。若对象 b 也没有属性 x,则查询它的原型对象 a 是否拥有属性 x。
该过程会一直持续,直到找到属性 x 或者查询到一个原型为 null 的对象。
属性访问错误
在访问对象的自有属性或继承属性时,若没有找该属性,则属性访问表达式的求值结果为 undefined 。比如,对象 o 有 sub-title 属性,没有 subtitle 属性:
o.subtitle // => undefined
在此基础之上,继续访问属性,会抛出 TypeError 错误:
o.subtitle.length // => TypeError
为了防止 TypeError 错误,可以通过
?.
(条件式属性访问)防止 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,删除属性xdelete 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有自有属性xo.hasOwnProperty(\'y\') // false,o没有属性yo.hasOwnProperty(\'toString\') // false,toString是继承属性
propertyIsEnumerable()
propertyIsEnumerable()方法用于测试对象的属性是否可枚举,方法参数接收对象的属性,该属性是自有属性且enumerable为true。某些内置属性是不可枚举的,常规创建的属性都是可枚举的,除非使它们限制为不可枚举:
let o = { x: 1 }o.propertyIsEnumerable(\'x\') // true,o有自有属性xo.propertyIsEnumerable(\'toString\') // false,toString不是自有属性Object.prototype.propertyIsEnumerable(\'toString\') // false,toString不可枚举
!==
除了使用上面几种操作符以外,还可以使用简单的
!==
测试属性是否存在于对象中:
let o = { x: 1 }o.x !== undefined // true,o有自有属性xo.y !== undefined // false,o没有属性yo.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\', { // 不可枚举属性vvalue: \'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\', { // 不可枚举属性vvalue: \'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\', { // 不可枚举属性vvalue: \'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\', { // 不可枚举属性vvalue: \'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 worldconsole.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) // 2wm[METHOD_NAME](1) // 3wm[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 = nelse throw new Error(\'serial number can only be set to a larger value\')}}serialnum.next = 10 // 初始化值serialnum.next // 10serialnum.next // 11,每次读取的值不同