属性的可枚举性

可枚举性

对象的每个属性都有一个描述对象(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函数的语句