属性的可枚举性
可枚举性
对象的每个属性都有一个描述对象(Desciprtor),用来控制该属性的行为。Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象
let obj = {foo: 'foo'}
Object.getOwnPropertyDescriptor(obj, 'foo')
/*
{
value: 'foo',
writable: true,
enumerable: true,
configurable: true
}
*/
描述对象的enumerable属性,称为’可枚举性’, 如果该属性为false,表示某些操作会忽略当前属性
目前,有四个操作会忽略enumerable为false的属性
for in 只遍历对象自身和继承的可枚举属性
object.keys 只返回可遍历的属性名
Json.stringify 只串行化对象自身的可枚举的属性
Object.assign 忽略enumerable为false的属性,只拷贝对象自身的可枚举属性
引入可枚举性这个概念的最初目的,就是让某些属性可以避免被for…in,否则内部的方法,属性都会被遍历到。比如对象的tostring,数组的length
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumable
// false
Object.getOwnPropertyDescriptor([], 'length').enumable
// false
上面代码对象的toString 和数组的length 属性的enumable 都是false,所以不会被for…in遍历到
ES6规定所有class的继承属性都是不可枚举的
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
由于for…in总是引入继承的属性,所以尽量使用Object.keys
属性的遍历
ES6一共有五种方法,可以遍历对象的属性
(1)for…in
for…in遍历对象自身和继承的可枚举的属性(不包含Symbol)
(2)Object.keys(obj)
Object.keys返回一个数组,包含自身所有(不含继承)可枚举的属性(不含Symbol)键名
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身所有属性(不含Symbol,含有不可枚举)属性的键名
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols 返回一个数组,包含自身所有包含Symbol属性的键名
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身所有属性(不含继承),不管键名是否是Symbol,不管是否可枚举
以上的5种方法遍历对象的键名,都遵循同样的属性遍历的次序规则
首先遍历所有数值键,按照数值升序排列
其次遍历所有字符串键,按照加入时间升序排列
最后遍历所有Symbol键,按照加入时间升序排列
let obj = {
[Symbol()]: 0,
b: 0,
3: 9,
10: 20,
a: 9
}
Reflect.ownKeys(obj)
// [3, 10, b, a, Symbol()]
Reflect.ownKeys方法会返回一个数组,这个数组的返回次序是这样的,
首先是数值3和10,其次是字符串b和a,最后是symbol
super关键字
this总是指向函数所在的当前对象,ES6新增了super关键字,它指代的是当前对象的原型对象
this --> 当前对象
super --> 当前对象的原型对象
let person = {
name: 'person'
}
let son = {
name: 'sun',
printPerson () {
return super.name
}
}
Object.setPrototypeOf(son, person)
son.printPerson()
// 'person'
上面通过设置son的原型,调用son方法来找到原型的属性
super表示原型对象时,只能用在对象的方法之中,用在其他地方会报错
let obj = {
foo: super.foo
}
// 用在了属性上
let obj1 = {
foo: () => super.foo
}
// 用在了方法
let obj2 = {
foo: function () {
return super.foo
}
}
上面三种都会报错,因为对于js引擎而言,根本没有用到super。
第一种属于用在属性中
第二种和第三种属于用在函数中,但又赋值给了foo属性
现在只有简写的对象方法才能让js引擎确认,定义的是对象的方法
js引擎内部,super.foo等同于
Object.getPrototypeOf(this).foo 属性 或 Object.getPrototypeOf(this).foo.call(this) 方法。
let father = {
name: 'person',
sayHello () {
return this.name
}
}
let son = {
name: 'son',
sayHi () {
return super.sayHello();
}
}
Object.setPrototypeOf(son, father);
son.sayHi();
// 'son'
上面代码分为三步
第一步调用son.sayHi 函数返回了原型的sayHello方法
第二步father自身调用sayHello方法,返回了this.name
第三部this.name 此时的this,因为在son的环境执行的所以,this指向son,所以打印结果为’son’
如果改为father.sayHello() 就不一样了
对象的扩展运算符
数组中的扩展运算符(…)以及得心应手了,对象的写法与之功能基本类似
解构赋值
对象解构用于将一个对象的值全部取出来(可遍历的),并且没有被读取的属性,赋值到另一个对象身上,所有他们的键和值,最后形成一个新对象
let {x, y, ...z} = {x: 1, y: 2, z: 3, u: 10, n: 20}
x // 1
y // 2
z // {z: 3, u: 10, n: 20}
上面代码只有z是解构成功的对象,x和y属于被读取过后的值了,z会把x y没有读取到的键和值拷贝过来,返回一个新数组
解构赋值等号右边必须是一个对象,如果不是就会报错
let {...y} = undefined // error
let {...n} = null // error
解构赋值必须是最后一个参数,否则会报错
let {...x, y} = obj; // error
let {x, ...y, z} = obj; // error
解构赋值是浅拷贝,即如果拷贝的值是一个数组,对象,函数,那么拷贝的实际是引用,而不是副本
let obj = { a: {b: 1} }
let {...newObj} = obj
newObj.a.b = 2;
obj.a.b
// 2
上面代码拷贝的是一个对象,由于解构只是浅拷贝,所以指向了同一个指针地址,导致新地址的数据改变,原地址指向的数据也要发生改变,因为他们的指向同一个房间。
解构赋值无法拿到原型的属性
let a1 = {bar: 'bar'}
let a2 = {foo: 'foo'}
Object.setPrototypeOf(a1, a2)
let {...a3} = a2
a3
// {foo: 'foo'}
a3.bar
// undefined
上面代码a2 继承a1,a3又拷贝了a2 ,但是解构赋值无法拿到原型属性,导致a3没有获取到a1这个祖先的属性
// 创建一个obj的原型对象
let obj = Object.create({x: 1, y: 2})
// 自身添加一个z属性
obj.z = 3
let {x, ...newObj} = obj
// x是单纯的解构赋值,所以可以读取继承属性。
// y和z是扩展运算符解构赋值,只能读取对象自身的属性
let {y, z} = newObj
// y是继承属性,所以newObj中获取不到。
x // 1
y // undefined
z // 3
可能有人觉得可以省事,会这么写
let {x, ...{y, z}} = obj
// error
ES6规定,变量声明语句中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式。
解构赋值的一个用处,是扩展某个函数的参数,传递给另一个函数或者引入其他操作
function test (a, b) {
return a + b
}
function bar (x, y, ...anther) {
// 使用x y 进行内部操作
// 把扩展运算符导出出去,提供给另一个函数,或者做别的操作
console.log(x * y);
return test(anther);
}
bar(1, 3, 9, 9)
扩展运算符
对象的扩展运算符,用于取出参数对象所有可遍历属性,拷贝到当前对象之中
let z = {x: 1, y: 2}
let n = {...z}
n // {x: 1, y: 2}
由于数组是特殊的对象,所以对象扩展运算符也可以用于数组
let foo = {...['x', 'y', 'z']}
foo
// {0: 'x', 1: 'y', 2: 'z'}
如果扩展运算符后面是一个空对象,则没有任何效果
let obj = {...{}, a: 1}
obj
// {a: 1}
如果不是个对象,会把他转换为对象
// {...Object(1)} 等同于
{...1}
// {}
上面会调用包装类,转换为Number(1) 但该对象自身本就没有任何属性,所以返回空
还有一些类型
{...true} // {}
{...undefined} // {}
{...null} // {}
{...'es6'} // {0: 'e', 1: 's', 2: '6'}
前三个都会返回空对象,最后一个字符串,由于字符串会转换为类数组,所以返回的就是一组带索引的对象
对象的扩展运算符等同于使用Object.assign() 方法
let aClone = {...a};
// 等同于
let aClone = Object.assign({}, a)
上面只是拷贝对象实例属性,想要拷贝原型属性,需要这样写
// fn1
let clone1 = {
// 拿到obj的原型,并赋给自己的proto, 自己的原型指向了obj的原型
__proto__ : Object.getPrototypeOf(obj),
// obj的实例属性赋给自己原型,拷贝obj的实例属性和原型属性
...obj
}
// fn2
let clone2 = Object.assign(
// 创建一个对象,该对象的原型是obj原型对象
Object.create(Object.getPrototypeOf(obj)),
// 把obj放到上面这个对象里
obj
)
// fn3
let clone3 = Object.create(
// 创建参照对象,该对象为obj的原型对象
Object.getPrototypeOf(obj),
// 把obj的所有可枚举属性传递给参照对象
Object.getOwnPropertyDescriptors(obj)
)
扩展运算符,可以合并对象
let obj1 = {...a, ...b}
let obj2 = Object.assign({}, a, b)
如果扩展运算符后面还有用户自定义的属性,那么扩展运算符内部的同名属性会被覆盖掉
let obj = {...a, x: 1, y: 2}
// 等同于
let obj1 = {...a, ...{x: 1, y: 2}}
// 等同于
let x = 1, y = 2, obj2 = {...a, x, y}
// 等同于
let obj3 = Object.assign({}, a, {x: 1, y: 2})
上面代码中,a对象中的x, y属性都会被 a后面的 x y都会在拷贝到新对象时覆盖掉
这样一来更改部分现有属性,很方便
let newVersion = {
...oldVersion,
name: 'new Version'
}
上面的旧版本 name会被替换掉
如果自定义属性放在扩展运算符前,就会变成设置新对象的默认属性值
let obj = {x: 1, y: 2, ...a}
==
let obj1 = Object.assign({}, {x: 1, y: 2}, a
==
let obj2 = Object.assign({x: 1, y: 2}, a)
对象扩展运算符后面也可以跟表达式
let obj = {
...(x > 1 : {name: 'wei'} ? {}),
bar: 'foo'
}
如果扩展运算符的参数对象之中,有取值函数get,它会自动执行的
let obj = {
get foo () {
console.log(1)
}
}
let newObj = {...obj}
// 1
上面代码,在扩展结束后就会执行get函数的语句