对象
对象 是一系列键值对的组合,因此对象有时候也叫 "散列表"、"字典"、"关联数组"、"映射" 等。对象可以从其他对象(原型)上继承属性。对象的方法通常是继承来的属性,原型式继承 是 JS 的主要特性。
JS 对象是动态可修改的,可以添加删除属性。将对象赋值给变量,则变量保存的是对象的一个 引用 ,而不是对象的副本。这意味着可以多个变量操作同一对象。
属性名可以为任何字符串或者符号,但是不能包含两个同名的属性。
对象的每个属性除了键值信息外,还有三个特性(值也是特性,为 value ):
writable(可写)指定是否可以设置属性的值;
enumerable(可枚举)指定是否可以在 for-in 循环中返回该属性;
configurable(可配置)指定是否可以删除属性,或修改属性特性。
创建对象
对象字面量
最简单的可以用对象字面量创建一个全新的对象:
let shop = {
name: "Talaxy's Shop",
items: ["apple", "banana", "pine", "melon"]
coordinates: {
x: 118.807859,
y: 31.935608
}
}
属性名可以是标识符、任意字符串或符号。即便是一模一样的字面量,每次创建的都是全新的对象。对象字面量所创建的对象的原型为 Object.prototype
,Object 是最基础的对象类型。
使用 new 创建对象
可以用 new 操作符跟上一个函数调用来创建对象,此时该函数称为构造函数:
let obj = new Object()
let arr = new Array()
对于上面的 Array ,其自身的原型 Array.prototype
也还会有个原型指向 Object 的原型。而 Object.prototype
的下个原型指向为 null
。这样的原型关系称为 原型链 :
// __proto__ 为对象的原型指向,开发中不建议使用该属性
Array.prototype.__proto__ === Object.prototype // => true
Object.prototype.__proto__ // => null
使用 Object.create()
可以用 Object.create()
创建对象,它会将传入的参数作为新对象原型:
let point = Object.create({ x: 1, y: 2 })
point.x + point.y // => 3
换句话讲,Object.create(Object.prototype)
与 {}
or new Object()
类似。而创建一个完全空对象(即连原型都没有)可以用 Object.create(null)
,它不会继承任何东西。
访问 & 设置属性
访问属性可以用点或中括号:
let point = { x: 1, y: 2 }
point["y"] // => 2
point.x += 1 // => 2
因为 JS 对象是关联数组,我们可以添加任意数量的属性。不过在 ES6 及后,使用 Map 类通常比用普通对象更好。
对于 Object.create()
新创建的对象,其自身还没有任何属性(除了原型指向),单纯的属性访问只是查询了原型对象的属性。但如果对新对象设置属性,则会先查询原型链上该属性是否可以赋值(而非只读),若允许赋值,则会从原型上拷贝属性到自身,再进行属性设置,而不会对原型进行修改:
let point = Object.create({ x: 1, y: 2 })
point // => {}
point.x // => 1
point.x += 1
point // => {x:2}
point.x // => 2
point.__proto__.x // => 1
查询未定义的属性不会报错,会返回 undefined
。而查询不存在的对象的属性则是错误(等价于访问 null
和 undefined
的属性,会抛出 TypeError )。所以在属性访问前先判断对象是否为空,或者用可选链 ?.
语法。
尝试在 null
和 undefined
设置属性也会导致 TypeError 。在非严格模式下,对只读属性赋值不会抛出 TypeError ,而严格模式会。
删除属性
删除属性可以用 delete
操作符。delete
只删除自有属性,不删除原型上的属性。不可配置的属性不会被删除,这类情况 delete
表达式会返回 false
,而严格模式下会抛出 TypeError 。
var
和 let
声明的全局属性是不可配置的,但是直接对 globalThis
设置的属性默认是可配置的。同时在严格模式下,删除全局对象的属性应当标明 globalThis
,而非直接使用非限定标识符:
"use strict"
globalThis.x = 1
delete x // ! SyntaxError
delete globalThis.x // => true
测试属性
检查对象是否有某个属性,除了判断其是否为 undefined
,还可以用 in
操作符、Reflect.has()
:
let point = { x: 1, y: 2 }
"x" in point // => true
Reflect.has(point, "x") // => true
检查是否有自有属性可以用 hasOwnProperty()
方法;检查是否可枚举可以用 propertyIsEnumerable()
方法。常规 JS 代码创建的对象都是可枚举的。
枚举属性
在用 for-in 对对象属性进行枚举时,从原型继承的属性也会被枚举,但是方法不可枚举的。可以用 hasOwnProperty()
方法过滤原型继承的属性。如果想避免枚举对象方法,可以用 typeof
操作符。
如果想用 for-of 枚举,可以用这四种函数获得属性名数组:
Object.keys()
返回自有的可枚举的属性名数组,不包括符号属性名;Object.getOwnPropertyNames()
返回自有的属性名数组,不包括符号属性名;Object.getOwnPropertySymbols()
返回自有的符号属性名数组;Reflect.ownKeys()
返回所有自有的属性名数组。
枚举顺序
ES6 中正式定义了枚举对象自有属性的顺序,以上四个方法以及 JSON.stringify()
等方法会按照以下顺序列出属性:
先列出名称为非负整数的字符串属性 。比如数组会按照正常索引顺序遍历;
再列出剩下的 字符串属性 ,按照 字面量出现顺序 以及 添加到对象的顺序 ;
最后列出 符号属性名 ,同样按照添加顺序。
而 for-in 的枚举顺序没有上述这么严格,但 JS 的实现通常会按照上述书顺序枚举自有属性,然后再沿原型链上溯(枚举过的属性不会再被枚举)。
扩展属性
通常可以用迭代属性的方式将一个对象的属性复制到另一个对象上。ES6 中可以用 Object.assign()
达到这一目的。它的第一个参数接收要扩展的目标对象,其余的参数均为属性扩展的来源对象。Object.assign()
会按照参数顺序,将来源对象的 可枚举自有属性 (包括符号)复制到目标对象上,同名属性会直接覆盖。
实际上属性复制会调用来源对象属性的 get 方法和目标对象属性的 set 方法,但这些方法本身不会被复制。更多可见 Object.assign() - MDN 。
如果想给一个对象填充默认值,可以先用默认值覆盖,再用自身覆盖:
obj = Object.assign({}, defaults, obj)
也可以用展开语法:
obj = { ...defaults, ...obj }
或者自己写一个避免覆盖的函数:
function merge(target, ...sources) {
for(let source of sources) {
for(let key of Object.keys(source)) {
if (!(key in target)) { // 避免覆盖
target[key] = source[key];
}
}
}
return target;
}
对象序列化
对象序列化 Serialization 是指把对象状态信息转为字符串的过程,之后也可以从字符串还原到对象状态。可以用 JSON.stringify()
和 JSON.parse()
来序列化及恢复,使用 JSON 数据交换格式。
JSON 即 JavaScript Object Notation ,JavaScript 对象表示法。
JSON 语法是 JS 语法的子集,它不能表示所有 JS 的值。可以序列化及恢复的值有:对象、数组、字符串、有限数值、true
、false
、null
。而 NaN、±Infinity 会被转为 null
。日期对象会调用其 toJSON()
方法转为 ISO 格式的日期字符串。函数、RegExp 对象、Error 对象以及 undefined
无法序列化,该类属性会被直接删除。
JSON.stringify()
和 JSON.parse()
都还可以接收第二个参数,用于自定义序列化及恢复操作。比如自定义哪些属性需要序列化、如何转换某些值等。
对象可以自定义一个 toJSON()
方法,JSON.stringify()
会在序列化的时候尝试寻找该方法并调用。
对象方法
所有的 JS 对象(除了 Object.create(null)
)都从 Object.prototype
继承了属性,这些属性主要是一些重要的方法。比如有:
toString()
返回代表对象的字符串。当需要把对象转为字符串时就会用到这个方法。默认的返回为
[object 类型名]
,由于不会显示太有用的信息,很多类都会重新定义自己的toString()
方法。toLocaleString()
返回代表对象的本地化字符串。比如 Date 就有自定义的
toLocaleString()
方法来根据当地的语言格式表示时间。valueOf()
返回代表对象的数值。
更多 Object 上定义的方法可见 Object - MDN 。
对象字面量扩展语法
属性简写
ES6 及后,如果在对象字面量中使用了变量,可以进行简写:
let x = 1
let obj = { x: x } // ES6 之前
let obj_2 = { x } // ES6 及后
计算属性名
ES6 支持表达式定义属性名,需用中括号包裹,且这种定义是动态而非预编译的:
let name = "name"
let person = {
[name]: "Talaxy" // 即 "name": "Talaxy"
}
符号属性
ES6 后,属性名可以是字符串或符号,可以通过计算属性名把符号作为属性名:
const extension = Symbol("an extension symbol")
let obj = {
[extension]: { /* ... */ }
}
符号始终是唯一的。使用符号作为属性名可以防止与自身的其他属性发生冲突,且不必担心第三方代码会意外使用你的符号属性。但这并不是安全的,因为可以使用 Object.getOwnPropertySymbols()
找到符号属性。
方法简写
在 ES6 及后,可以省略 function 声明来定义方法:
let square = {
side: 10,
perimeter: function() { return this.side * 4 } // ES6 之前
area() { return side * side } // ES6 及后
}
除了标识符,也可以用在计算属性名上。
访问器属性
可以通过 get 或 set 来为一个访问器属性定义其获取或设置方法:
let obj = {
_data: 10, // 下划线提示它仅在内部使用
get data() { return this._data },
set data(value) { this._data = value }
}
obj.data // => 10
obj.data = 20 // obj._data 为 20
与数据属性一样,访问器属性也可以继承。
对象展开
ES2018 及后,可以在对象字面量里使用 ...
操作符把已有对象的属性进行展开:
let coordinates = { x: 1, y: 2 }
let dimensions = { width: 100, height: 200 }
let rect = { ...coordinates, ...dimensions } // 会有四个属性
展开语法只展开对象的自有属性,同时按照顺序对新对象进行属性覆盖。使用的时候需注意性能问题。