前言
平常使用Object的频率非常高,但是使用它自带的一些API时,会有一些潜在的知识点,这些知识点可能会是影响bug产生的原因,接下来我将一一总结。
Object.keys、Object.values、Object.entries
在使用Object.keys
、Object.values
和 Object.entries
遍历对象自身属性的时候,原形上的属性不会被遍历出来。
js
const obj = Object.create({
// 原型链不会被遍历
age: 21
}, {
name: {
value: '1in',
enumerable: true,
}
});
Object.keys(obj); // ["1in"]
这三个函数的筛选逻辑本质上是一致的,都是调用了一个叫做 EnumerableOwnProperties()
的内部方法,只不过输出的数据不同,一个是所有的键,一个是所有的值,最后一个是键值。
我们从这个内部方法的名字就可以看出,它只会遍历可枚举
的属性,所以当对象的属性修饰符enumerable
为false时,其属性是不会出现在遍历的结果中的
js
const obj = Object.create(null, {
name: {
value: '1in',
enumerable: true,
},
age: {
value: '21',
// 不可枚举
enumerable: false,
}
});
Object.keys(obj); // ["1in"]
注意!!!我们都知道,Object的key值可以是string类型或者是symbol类型,这三种方法的遍历是不会遍历出key值为symbol类型的属性的。
js
const obj = {
name: '1in',
// Symbol 不输出
[Symbol('age')]: 21,
};
Object.keys(obj); // ["1in"]
所以,我们可以总结出 Object.keys/values/entries 只会遍历出对象自身的、可枚举的、以字符串类型为键的属性
。
遍历对象自身所有属性
经过上边的知识点,我们知道Object.keys/values/entries 只会遍历出对象自身的、可枚举的、以字符串类型为键的属性
。
所以,我们现在需要遍历出自身的所有数据,用什么方法呢?
Object.getOwnPropertyNames
可以用来获取其中的字符串键。Object.getOwnPropertySymbols
用来获取其中的 Symbol
键。
把他们两个结合起来,不就遍历出对象的所有属性了吗。
js
var obj = Object.create(null, {
[Symbol('name')]: {
value: '1in',
writable: false,
enumerable: true,
configurable: true,
},
age: {
value: '21',
writable: true,
enumerable: true,
configurable: true,
},
});
console.log([
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj),
]); // ["age", Symbol(name)]
但是,这样总感觉怪怪的。
因为,Object.getOwnPropertyNames
是 ES5 引入的,当时还没有 Symbol 类型,因此它只会返回一个字符串数组。ES6 引入 Symbol 之后,如果要求 Object.getOwnPropertyNames
也返回 Symbol 类型的话,那么恐怕很多代码都会出错。所以为了向后兼容的考量,又引入了一个 Object.getOwnPropertySymbols
专门返回 Symbol 类型的键。
当然,我们也有一个方法可以直接返回对象的key值为string类型或者symbol类型的方法。
ES6 引入了一个 Reflect.ownKeys
函数。
js
//接着上边的代码
console.log(Reflect.ownKeys(obj)); // ["age", Symbol(name)]
与此效果差不多的还有 Object.getOwnPropertyDescriptors
,但是它还会额外返回每一个键属性修饰符。
js
// {
// age: { value: '21', writable: true, enumerable: true, configurable: true },
// [Symbol(name)]: { value: '1in', writable: false, enumerable: true, configurable: true }
// }
console.log(Object.getOwnPropertyDescriptors(obj));
遍历对象及它的原型链上的可枚举属性
能够实现遍历原型链的现成方法,目前只有 for...in
一种。
但是,它只能遍历可枚举、字符串键的属性。
js
var obj = Object.create(
Object.create(null, {
// 原型链上的属性会被遍历
name: {
value: '1in',
writable: true,
enumerable: true,
configurable: true,
},
}), {
// Symbol 不会被遍历
[Symbol('age')]: {
value: '21',
writable: false,
enumerable: true,
configurable: true,
},
school: {
value: 'cdut',
writable: true,
enumerable: true,
configurable: true,
},
// 不可枚举的属性不会被遍历
company: {
value: 'alibaba',
writable: true,
enumerable: false,
configurable: true,
},
}
);
for (let key in obj) {
console.log(key); // 1in cdut
}
如果想把 Symbol
包括进来,甚至和那些不可枚举的属性,只能自己实现。下面就是一种未经过优化的代码,仅代表其可能性。
js
function getExtendedKeys(obj) {
const visitedKeys = new Set();
let current = obj;
// 向上遍历原型链
while (current) {
// 遍历当前属性
const keys = Reflect.ownKeys(current);
keys.forEach(key => {
visitedKeys.add(key);
});
current = Object.getPrototypeOf(current);
}
return Array.from(visitedKeys);
}
原理是原型链遍历和属性遍历。
对象合并
一般的,我们合并两个对象时,会采用Object.assign()
和 Object Spread(对象展开)
这两种方式。 Object Spread(对象展开)大家可能一时反应不过来,下边是我们使用的方法。
js
//Object Spread(对象展开)
const a={
name:"1in"
}
const b={
age:21
}
const c={
...a,
...b
}
然而事实上,它们的原理完全不同。
Object.assign 以 set 的方式赋值属性,而 Object Spread 以 defineProperty 的方式定义属性
。
js
function assign(dest, src) {
for (let key in src) {
// 跳过非自身属性
if (!src.hasOwnProperty(key)) continue;
// set
dest[key] = src[key];
}
}
function spread(dest, src) {
for (let key in src) {
// 跳过非自身属性
if (!src.hasOwnProperty(key)) continue;
// defineProperty
Object.defineProperty(dest, key, {
value: src[key],
writable: true,
enumerable: true,
configurable: true,
})
}
}
Object.assign()
将源对象的可枚举属性都取出来,直接赋值给目标对象;Object Spread
语法也是将源对象的可枚举属性都取出来,不过是在目标对象上定义一个数据属性。
Object.assign()
可能会将数据赋值到目标对象的原型上,如果原型上有这个 key 的存取器属性
的话;
Object Spread
抛弃了源对象属性的描述符,无论它是数据属性还是存取器属性,无论是可配置的还是不可配置的,也无论是可枚举的还是不可枚举的,最终都转换为目标对象上的一个可枚举、可配置、可写的数据属性
。
js
var _name = null;
var dest = Object.create({
set name(n){
_name = n;
},
get name(){
return _name;
},
});
Object.assign(dest, {
name: '1in'
});
console.log(dest.name); // "bar"
// Object.assign 赋值到了对象的原型上而非对象本身
console.log(Object.getOwnPropertyDescriptor(dest, 'name')); // undefined
js
var source = Object.create(null, {
name: {
get() {
return '1in';
},
set(){},
enumerable: true,
configurable: false,
},
});
const dest = { ...source };
// Object Spread 在目标对象上定义可配置的数据属性
console.log(Object.getOwnPropertyDescriptor(dest, 'name')); // { value: '1in', writable: true, enumerable: true, configurable: true }
所以object.assign会有以下报错场景:
- 如果目标对象现存属性是只读的,Object.assign 可能会失败。
- 如果目标对象现存属性是不可配置的,或者对象不可扩展,那么 Object.assign 可能会失败。
如果来源对象有问题的话(比如只有 set 没有 get),两种语法都会有共同的报错情景。
这两种方法都是操作批量属性的,如果其中某一属性合并失败,那么之前已经合并的属性会保留,不会回滚,因此,合并失败是可能产生未知的对象污染的
。