对象的扩展

属性名表达式

JavaScript 定义属性有两种方法:

1
2
obj.foo = true
obj["a" + "bc"] = 123

ES6 允许字面量定义对象时使用第二种方法:

1
2
3
4
5
6
7
8
let propKey = "foo"
let obj = {
[propKey]: true,
["a" + "bc"]: 123,
["he" + "llo"]() {
return "hi"
},
}

方法的 name 属性

方法的 name 属性也返回函数名。

1
2
3
4
5
6
const person = {
sayName() {
console.log("hello")
},
}
person.sayName.name //'sayName'

如果对象的方法使用了 getter 和 setter,则 name 的属性不是在该方法上面,而是在该方法属性的描述对象的 get 和 set 属性上面,返回值是方法名前面加上 get 和 set

1
2
3
4
5
6
7
8
9
const obj = {
get foo() {},
set foo(x) {},
}
// obj.foo.name; //TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, "foo")
descriptor.get.name //'get foo'
descriptor.set.name //'set foo'

如果对象的方法是一个 Symbol 值,那么 name 属性返回的是这个 Symbol 值的描述。

1
2
3
4
5
6
7
8
const key1 = Symbol("description")
const key2 = Symbol()
let obj = {
[key1]() {},
[key2]() {},
}
obj[key1].name //'[description]'
obj[key2].name //''

Object.is()

ES5 比较两个值是否相等,只有两个相等运算符:==和===,他们都有个缺点,前者会自动转换数据类型,后者 NaN 不等于自身,以及+0 等于-0。
ES6 提出了Same-value-equality算法来解决这个问题,Object.is()就是部署这个算法的新方法。
不同之处只有两个:

1
2
3
4
;+0 === -0 //true
NaN === NaN //false
Object.is(+0, -0) //false
Object.is(NaN, NaN) //true

ES5 可以用代码部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(Object, 'is' {
value : function(x, y) {
if (x === y) {
//针对+0不等于-0的情况
return x !== 0 || 1 / x === 1 / y;
}
//针对NaN
return x !== x && y !== y;
},
configurable : true,
enumerable : false,
writable : true
});

Object.assign()

Object.assign 方法用于将源对象的所有 可枚举 属性复制到目标对象。第一个参数是目标对象,后面的参数都是源对象

1
2
3
4
5
var target = { a: 1 }
var source1 = { b: 2 }
var source2 = { c: 3 }
Object.assign(target, source1, source2)
target //{ a : 1, b : 2, c : 3 }

如果只有一个参数,Object.assign()会直接返回该参数。
如果参数不是对象,会先转成对象,然后返回,由于 undefined 和 null 无法转变,所以如果将他们作为参数,就会报错。但是,如果 undefined 和 null 不在首参数,那就不会报错。
其他类型的值(数值、字符串和布尔值)不在首参数也不会报错,但是,除了字符串会以数组形式复制到目标对象,其他值都不会产生效果。
Object.assign()方法实行的赋值是浅复制,如果源对象的某个属性的值是对象,那么目标对象得到的是这个对象的引用

1
2
3
4
var obj1 = { a: { b: 1 } }
var obj2 = Object.assign(obj2, obj1)
obj1.a.b = 2
obj2.a.b //2

对于这种嵌套对象,一旦遇到同名属性,处理的方法是替换而不是添加。

1
2
3
4
var target = { a: { b: "c", d: "e" } }
var source = { a: { b: "hello" } }
Object.assign(target.source)
//target对象的a属性被source对象的a属性整个替换掉了

Object.assign()可以用来处理数组,但是会把数组当成对象来处理。

1
Object.assign([1, 2, 3], [4, 5]) //[4, 5, 3]

常见用途:

  • 为对象添加属性:
1
2
3
4
5
class Point {
constructor(x, y) {
Object.apply(this, { x, y })
}
}
  • 为对象添加方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {},
anotherMethod() {
//...
},
})
//等同于
SomeClass.prototype.someMethod = function (arg1, arg2) {
//...
}
SomeClass.prototype.anotherMethod = function () {
//...
}
  • 克隆对象
1
2
3
4
5
6
7
8
9
10
//只克隆对象自身值
function clone(origin) {
Object.assign({}, origin)
}

//保持继承链的方法
function clone(origin) {
let originProto = Object.getPrototypeOf(origin)
return Object.assign(Object.create(originProto), origin)
}
  • 合并多个对象
1
2
3
4
const merge = (target, ...sources) => Object.assign(target, ...sources)

//合并到一个对象
const merge = (...source) => Object.assign({}, ...source)
  • 为属性指定默认值
1
2
3
4
5
6
7
8
9
const DEFAULTS = {
logLevel: 0,
outputFormat: "html",
}
function processContent(options) {
options = Object.assign({}, DEFAULTS, options)
console.log(options)
//...
}

属性的可枚举性

对象的每一个属性都有一描述对象,用于控制该属性的行为,Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象。

1
2
3
4
5
6
7
8
let obj = { foo: 123 }
Object.getOwnPropertyDescriptor(obj, "foo")
// {
// value : 123,
// writable : true,
// enumerable : true,
// configurable : false
// }

enumerable 属性成为可枚举性,如果该属性为 false,就表示某些操作会忽略当前属性。
ES5 有三个操作会忽略 enumerable 为 false 的属性:

  • for…in 循环:只遍历对象自身和继承的可枚举属性。
  • Object.keys(): 返回对象自身的所有可枚举属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举属性。

ES6 规定,所有 Class 的原型的方法都是不可枚举的

属性的遍历

ES6 共有五种方法遍历对象的属性:

  • for…in 循环:遍历对象自身和继承的可枚举属性(不含 Symbol 属性)。
  • Object.keys():返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)。
  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)。
  • Object.getOwnPropertySymbols(obj): 返回一个数组,包含对象自身的所有 Symbol 属性。
  • Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有属性,不管属性名是 Symbol 还是字符串,也不管是否可枚举。
    以上方法遍历对象属性时都遵守同样的属性遍历次序规则:
  • 首先遍历所有属性名为数值的属性,按照数字排序。
  • 其次遍历所有属性名为字符串的属性,按照生成时间排序。
  • 最后遍历所有属性名为 Symbol 值的属性,按照生成时间排序。

proto属性

proto属性用来读取或设置当前对象的 prototype 对象。

1
2
3
4
5
6
7
8
9
//ES6写法
var obj = {
method: function () {},
}
obj.__proto__ = someOtherObj

//ES5写法
var obj = Object.create(someOtherObj)
obj.method = function () {}

在实现上,proto调用的是 Object.prototype.proto,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Object.defineProperty(Object.prototype, "__proto__", {
get() {
let _thisObj = Object(this)
return Object.getPrototypeOf(_thisObj)
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError()
}
if (!isObject(this)) {
return undefined
}
if (!isObject(proto)) {
return undefined
}
let status = Reflect.setPrototypeOf(this, proto)
if (!status) {
throw new TypeError()
}
},
})
function isObject(value) {
return Object(value) === value
}

如果一个对象本身部署了proto属性,则该属性的值就是对象的原型。

1
Object.getPrototypeOf({ __proto__: null }) //null

Object.setPrototypeOf()

Object.setPrototypeOf()用于设置一个对象的 prototype 对象,返回参数本身。

1
2
3
4
5
6
7
8
let proto = {}
let obj = { x: 10 }
Object.setPrototypeOf(obj, proto)
proto.y = 20
proto.z = 30
//obj对象可以读取proto对象的属性
obj.y //20
obj.z //30

如果第一个参数不是对象,就会自动转为对象,但是由于返回的还是第一个参数,所以这个操作不会有任何的结果。
如果第一个参数是 undefined 或 null,则会报错

Object.setPrototypeOf()

Object.setPrototypeOf()用于读取一个对象的 prototype 对象。
如果第一个参数不是对象,则会自动转为对象。
如果第一个参数是 undefined 或 null,则会报错

Object.keys()、Object.values()、Object.entries()

Object.keys()返回一个数组,成员是参数对象自身的(不含继承)所有可遍历属性的键名。
Object.values()返回一个数组,成员是参数对象自身的(不含继承)所有可遍历属性的键值。
Object.entries()返回一个数组,成员是参数对象自身的(不含继承)所有可遍历属性的键值对数组。
实现 Object.entries():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Generator函数版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]]
}
}

//非Generator函数版本
function entries(obj) {
let arr = []
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]])
}
return arr
}

对象的扩展运算符

1
2
3
4
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }
x //1
y //2
z //{ a : 3, b : 4 }

扩展运算符可以用于合并两个对象

1
2
3
let ab = { ...a, ...b }
//等同于
let ab = Object.assign({}, a, b)

Object.getOwnPropertyDescriptors()

ES5 的 Object.getOwnPropertyDescriptor()方法用来返回某个对象属性的 descriptor。

1
2
3
4
5
6
7
8
var obj = { p: "a" }
Object.getOwnPropertyDescriptor(obj, "p")
// Object {
// value : 'a',
// writable : true,
// enumerable: true,
// configurable: true
// }

ES2017 引入了 Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承)的 descriptor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const obj = {
foo: 123,
get bar() {
return this.foo
},
}
Object.getOwnPropertyDescriptors(obj)
// {
// foo: {
// value : 123,
// writable: true,
// enumerable: true,
// configurable: true
// },
// bar: {
// get: [Function bar],
// set: undefined,
// enumerable: true,
// configurable: true
// }
// }

使用 Object.prototype.toString()进行类型判断

Object.prototype能够更好的识别出类型,typeof只能识别出基础类型。

1
2
3
4
let date = new Date()

console.log(typeof date) // object
console.log(Object.prototype.toString.call(date)) // [object Date]

可以识别的类型有:

  • 123: [object Number]
  • ‘1234’: [object String]
  • true: [object Boolean]
  • undefined: [object Undefined]
  • null: [object null]
  • {foo: ‘bar’}: [object Object]
  • [1, 2, 3]: [object Array]
  • new Date(): [object Date]
  • new Error(): [object Error]
  • /a+/g: [object RegExp]
  • function a() {}: [object Function]
  • Math: [object Math]
  • JSON: [object JSON]
  • arguments: [object arguments]