跳到主要内容

对象

对象 是一系列键值对的组合,因此对象有时候也叫 "散列表"、"字典"、"关联数组"、"映射" 等。对象可以从其他对象(原型)上继承属性。对象的方法通常是继承来的属性,原型式继承 是 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 。而查询不存在的对象的属性则是错误(等价于访问 nullundefined 的属性,会抛出 TypeError )。所以在属性访问前先判断对象是否为空,或者用可选链 ?. 语法。

尝试在 nullundefined 设置属性也会导致 TypeError 。在非严格模式下,对只读属性赋值不会抛出 TypeError ,而严格模式会。

删除属性

删除属性可以用 delete 操作符。delete 只删除自有属性,不删除原型上的属性。不可配置的属性不会被删除,这类情况 delete 表达式会返回 false ,而严格模式下会抛出 TypeError 。

varlet 声明的全局属性是不可配置的,但是直接对 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() 等方法会按照以下顺序列出属性:

  1. 先列出名称为非负整数的字符串属性 。比如数组会按照正常索引顺序遍历;

  2. 再列出剩下的 字符串属性 ,按照 字面量出现顺序 以及 添加到对象的顺序

  3. 最后列出 符号属性名 ,同样按照添加顺序。

而 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 的值。可以序列化及恢复的值有:对象、数组、字符串、有限数值、truefalsenull 。而 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 } // 会有四个属性

展开语法只展开对象的自有属性,同时按照顺序对新对象进行属性覆盖。使用的时候需注意性能问题。

参考

Object - MDN

Object.assign() - MDN