对象扩展
1. 属性的简洁表示法
ES6 允许直接写入变量和方法,作为对象的属性和方法,需要注意的是简洁写法的属性名总是字符串
const obj = {
class () {}
}
// 这里的 class 是字符串,所以不会因为它属于关键字,而导致语法解析报错
var obj = {
'class': function () {}
}
// 如果某个方法的值是一个 Generator 函数,前面需要加上星号
const obj = {
* m() {
yield 'hello world'
}
}
2. 属性名表达式
ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内
let obj = [
[name]: 'han',
['a' + 'ge']: 21,
'last name': 'haiguang'
]
表达式还可以用在定义方法名上
let obj = {
['h' + 'ello']() {
return 'hi'
}
}
obj.hello() // hi
注意:属性名和简洁表示法,不能同时使用,会报错。还有就是属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object],这一点需要特别小心
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
3. 方法的 name 属性
函数的 name 属性,返回函数名,对象方法也是函数,因此也有 name 属性,如:obj.sayName.name
就返回 sayName
如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。
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"
还有几种特殊情况:
1. bind 方法创造的函数u,name 属性返回 bound 加上原函数的名字
2. Function 构造函数,name 属性返回 anonymous
3. 如果对象的方法是一个 Symbol 值,那么 name 属性返回的是这个 Symbol 值得描述
const key1 = Symbol('description');
let obj = {
[key1]() {}
};
obj[key1].name // "[description]"
4. Object.is()
用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
不同之处有两个:
1. +0 不等于 -0
2. NaN 等于自身
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
5. Object.assign()
用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。(参数:第一个是目标对象,后面的参数都是源对象)
注意:在合并过程中,如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。而且 undeifned 和 null 无法转为对象,所以他们作为参数的话会出现报错。当时如果这两种值没有作为首参数的话,他们会被跳过,这时候又不会报错。
同样的其他类型的值(数值、字符串、布尔值)不在首参数的话,也不会报错。字符串则会以数组形式拷贝入目标对象
同时,这个方法只拷贝对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性
注意点
1. 浅拷贝:Object.assign 方法实行的是浅拷贝,不是深拷贝。所以如果源对象的某个属性的值是对象,那么只是拷贝了这个对象的引用
2. 同名属性的替换
3. 数组的处理:这个方法会把数组视为属性名为0、1、2的对象,这里如果是多个数组的合并,会出现相同key值被覆盖掉的情况
4. 取值函数的处理:如果要复制的值是一个取值函数,那么将求值后再进行复制
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3],相同的 key 值被替换
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 },这里的 source 对象的 foo 属性是一个取值函数,这里会直接拿到值以后,将这个值复制过去
常见用途(重要多看几遍)
1. 为对象添加属性,如在一个类的构造函数中,将对象的属性添加到类的对象实例上面Object.assign(this, {x, y})
2. 为对象添加方法,如直接将一个对象的原型对象作为目标对象,然后源对象是这种形式:{fnA (a, b) {...}, fnB (c) {..}}
,这相当于直接给这个对象添加原型方法
3. 克隆对象,这个是比较常用的方法
4. 为属性指定默认值(这里需要留意这个方法只是浅拷贝的问题)
// 为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
6. 属性的可枚举性和遍历
可枚举性:对象的每个属性都有一个描述对象,用来控制该属性的行为(Object.getOwnPropertyDescriptor),这个描述对象的 enumerable 属性,成为可枚举性,如果这个属性为 false,表示某些操作会忽略当前的属性,如以下几种操作:
- for…in 循环(相比于其他三个方法,这个方法可以返回继承的属性,其他的则不会)
- Object.keys()
- JSON.stringify():只串行化对象自身的可枚举的属性
- Object.assign
属性的遍历
1. for..in:循环遍历对象自身的和继承的可枚举属性(不含Symbol)
2. Object.keys(obj):返回一个数组,包括对象的(不含继承的)所有可枚举属性(不含Symbol属性)的键名
3. Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含Symbol属性,包含不可枚举属性)的键名
4. Object.getOwnPropertySymbols(obj):返回一个数组,包含对象的所有Symbol属性的键名
5. Reflect.ownKeys(obj):返回一个数组,包含对象的自身所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
他们都遵守同样的属性遍历次序规则
- 首先遍历所有数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有 Symbol 键,按照加入时间升序排列
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
7. Object.getOwnPropertyDescriptors()
该方法会返回指定对象所有自身属性(非继承属性)的描述对象。所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象
该方法引入的目的在于
1. 解决 Object.assign() 无法正确拷贝 get 属性和 set 属性的问题。
2. 还有就是配合 Object.create 方法,将对象属性克隆到一个新对象(浅拷贝)
// 两个对象合并为一个对象
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
)
8. proto 属性,Object.setPrototypeOf(),Object.getPrototypeOf()
_proto_属性
用来读取或设置当前对象的 prototype 对象,但是最好不要使用这个属性,可以使用以下属性来进行替代:
1. Object.setPrototypeOf():写操作
2. Object.getPrototypeOf():读操作
3. Object.create():生成操作
Object.setPrototypeOf()
用来设置一个对象的 prototype 对象,返回参数对象本身(ES6中正式推荐的设置原型对象的方法)
格式:
Object.setPrototypeOf(object, prototype)
// 将 proto 对象设为 obj 对象的原型,所以 obj 对象可以读取 proto 对象的属性
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
注意:如果第一个参数不是对象,会自动转为对象,但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。而且由于 undefined 和 null 无法转为对象,所以如果第一个参数是 undefined 或 null,就会报错
Object.getPrototypeOf()
用来读取一个对象的原型对象,同样的,如果参数不是对象,会被自动转为对象,如果参数是 undefined 或者 null,就会报错
9. super 关键字
这个关键字指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
注意:super 关键字表示原型对象时,只能用在对象的方法中,用在其他地方都会报错。目前只有对象方法的简写可以让 JavaScript 引擎确认,定义的对象的方法
JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)
10. Object.keys(),Object.values(),Object.entries()
返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名、或值(或键值对数组)
这个方法和 Object.value 和 Object.entries 作为遍历一个对象的补充手段,供 for…of 循环使用
如果参数不是对象,都会被先转为对象。由于数值和布尔值的包装对象都不会为实例添加非继承的属性,所以会返回空数组
// 注意这里的返回顺序规则
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
关于 Object.entries 方法的另一个用处是,将对象转为真正的 Map 结构
const obj = {foo: 'bar', baz: 22}
const map = new Map(Object.entries(obj))
map // Map { foo: 'bar', baz: 22 }
11. 对象的扩展运算符
利用扩展运算符进行解构赋值,如:let {x, y, z...} = {x: 1, y: 2, a: 3, b: 4}
,这里的 z 最终为 { a: 3, b: 4 }
注意:解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值得引用,而不是这个值得副本。(同时,并不能复制继承自原型对象的属性)
// 理解
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3
如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式
// 报错
let { x, ...{ y, z } } = 0
这个方法等同于 Object.assign 方法
let aClone = {...a}
// 相当于
let aClone = Object.assign({}, a)
// 可以用于合并两个对象
let ab = { ...a, ...b }
// 相当于
let ab = Object.assign({}, a, b)
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖
// 用来修改现有对象部分的属性很方便
let newVersion = {
...previousVersion,
name: 'New name'
}
// name 可以覆盖掉 previousVersion 里面的同名属性
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值
扩展运算符的参数对象中,如果有取值函数 get,这个函数是会执行的
// 并不会抛出错误,因为 x 属性只是被定义,但没执行
let aWithXGetter = {
...a,
get x() {
throw new Error('not throw yet');
}
};
// 会抛出错误,因为 x 属性被执行了
let runtimeError = {
...a,
...{
get x() {
throw new Error('throw now');
}
}
};