1 属性的可枚举性和遍历
<script>
const obj = {
userName: 'zhaoshuai-lc',
userAge: 26,
userSex: 'male'
}
let res = Object.getOwnPropertyDescriptors(obj);
console.log(res);
</script>
描述对象的 [ enumerable ] 属性, 称为“可枚举性”, 如果该属性为 [ false ], 就表示某些操作会忽略当前属性.
1 for...in循环: 只遍历对象自身的和继承的可枚举的属性.
2 Object.keys(): 返回对象自身的所有可枚举的属性的键名.
3 JSON.stringify(): 只串行化对象自身的可枚举的属性.
4 Object.assign(): 忽略enumerable为false的属性, 只拷贝对象自身的可枚举的属性.
<script>
const obj = {
userName: 'zhaoshuai-lc',
userAge: 26,
userSex: 'male'
}
for (const key in obj) {
console.log(key, " : ", obj[key]);
}
// return string[]
let objStr = Object.keys(obj);
console.log(objStr);
// return string
let json = JSON.stringify(obj);
console.log(json);
</script>
这四个操作之中, 前三个是 ES5 就有的, 最后一个 [ Object.assign() ] 是 ES6 新增的. 其中, 只有for…in会返回继承的属性, 其他三个方法都会忽略继承的属性, 只处理对象自身的属性. 实际上, 引入“可枚举”(enumerable)这个概念的最初目的, 就是让某些属性可以规避掉for…in操作, 不然所有内部属性和方法都会被遍历到. 比如, 对象原型的toString方法, 以及数组的length属性, 就通过“可枚举性”, 从而避免被for…in遍历到.
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
上面代码中, toString和length属性的enumerable都是false, 因此for…in不会遍历到这两个继承自原型的属性.
另外 , ES6 规定, 所有 Class 的原型的方法都是不可枚举的.
Object.getOwnPropertyDescriptor(class {
foo() {
}
}.prototype, 'foo').enumerable
// false
总的来说, 操作中引入继承的属性会让问题复杂化, 大多数时候, 我们只关心对象自身的属性. 所以, 尽量不要用 [ for…in ] 循环, 而用 [ Object.keys() ] 代替.
2 属性的遍历方法
<script>
const obj = {
userName: 'zhaoshuai-lc',
userAge: 26,
userSex: 'male'
}
// for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性).
console.log('const key in obj')
for (const key in obj) {
console.log(key, " : ", obj[key]);
}
// return string[]
// Object.keys返回一个数组, 包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名.
let objStr = Object.keys(obj);
console.log('Object.keys(obj) : ', objStr);
// return string[]
// Object.getOwnPropertyNames返回一个数组, 包含对象自身的所有属性(不含 Symbol 属性, 但是包括不可枚举属性)的键名.
let propertyNames = Object.getOwnPropertyNames(obj);
console.log('Object.getOwnPropertyNames(obj) : ', propertyNames);
// return symbol[]
// Object.getOwnPropertySymbols返回一个数组, 包含对象自身的所有 Symbol 属性的键名.
let propertySymbols = Object.getOwnPropertySymbols(obj);
console.log('Object.getOwnPropertySymbols(obj) : ', propertySymbols);
// return (string | symbol)[]
// Reflect.ownKeys返回一个数组, 包含对象自身的(不含继承的)所有键名, 不管键名是 Symbol 或字符串, 也不管是否可枚举.
let ownKeys = Reflect.ownKeys(obj);
console.log('Reflect.ownKeys(obj) : ', ownKeys);
</script>
3 super 关键字
我们知道, this关键字总是指向函数所在的当前对象 , ES6 又新增了另一个类似的关键字 [ super ], 指向当前对象的原型对象.
1 原型链实现继承 (bad)
<script>
<!-- 父类型-->
function Supper() {
this.supPro = 'Supper property'
}
// 定义子类型
function Sub() {
this.subPro = 'Sub property'
}
Sub.prototype = new Supper()
Sub.prototype.constructor = Sub
let sub = new Sub();
console.log('sub.supPro ', sub.supPro);
</script>
<script>
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child
//实例化两个对象
let s1 = new Child();
let s2 = new Child();
console.log(s1.play, s2.play) //[1, 2, 3] [1, 2, 3]
//修改其中一个的原型
s1.play.push(4)
console.log('s1.play ', s1.play)
console.log('s2.play ', s2.play)
</script>
当我们改变S1的属性时,另外一个实例也发生了改变,这是因为这两个实例使用的是同一个原型对象,他们的内存空间是共享的
2 构造函数继承(bad)
<script>
function Parent() {
this.name = 'Parent'
this.play = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name
}
//定义构造函数
function Child() {
Parent.call(this);
this.type = 'Child'
}
let child = new Child()
let _child = new Child()
child.play.push(100)
console.log('child ', child)
console.log('_child ', _child)
child.getName() //报错
</script>
相比第一种原型链继承方式,父类的引用属性不会被共享,但他优化了第一种继承方式的弊端
只能继承父类的实例属性和方法,不能继承其原型属性或方法
3 组合继承
前面我们讲到两种继承方式,各有优缺点.组合继承则是将这两种方式结合起来: 原型链继承+构造函数继承
<script>
function Parent() {
this.name = 'Parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this);
this.type = 'Child'
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let s1 = new Child();
let s2 = new Child();
console.log('s1.play ', s1.play)
console.log('s2.play ', s2.play)
s1.play.push(4)
console.log('s1.play ', s1.play)
console.log('s2.play ', s2.play)
console.log('s1.getName() ', s1.getName())
console.log('s2.getName() ', s2.getName())
</script>
这种方式看起来没什么问题,但实际上 Parent 执行了两次,造成了多构造一次的性能开销。
4 super
我们知道, this关键字总是指向函数所在的当前对象 , ES6 又新增了另一个类似的关键字 [ super ], 指向当前对象的原型对象.
<script>
const proto = {foo: 'hello'};
const obj = {
foo: 'world',
find() {
return super.foo;
},
_find() {
return this.foo;
}
};
Object.setPrototypeOf(obj, proto);
console.log(obj.__proto__);
console.log('obj.find() ', obj.find());
console.log('obj._find() ', obj._find());
</script>
上面代码中, 对象obj.find()方法之中, 通过super.foo引用了原型对象proto的foo属性.
注意, super关键字表示原型对象时, 只能用在对象的方法之中, 用在其他地方都会报错.
<script>
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
</script>
上面三种super的用法都会报错, 因为对于 JavaScript 引擎来说, 这里的super都没有用在对象的方法之中. 第一种写法是super用在属性里面; 第二种和第三种写法是super用在一个函数里面, 然后赋值给foo属性. 目前, 只有对象方法的简写法可以让 JavaScript 引擎确认, 定义的是对象的方法.
<script>
const proto = {
x: 'hello',
foo() {
console.log('proto ', this.x);
},
};
const obj = {
x: 'world',
foo() {
console.log('obj ')
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // world
</script>
上面代码中, super.foo()
指向原型对象proto
的foo()
方法, 但是绑定的this却还是当前对象obj, 因此输出的就是world.