1、对象属性的分类
1-1、数据属性和访问器属性
1-1-1. 数据属性的特性:
- configurable(是否可以修改属性的特性,是否可以删除属性);
- enumerable(是否可枚举,是否可以通过 for-in 循环返回);
- writable(属性的值是否可以被修改);
- value(属性值);
1-1-2. 访问器属性的特性:
- configurable(同数据属性);
- enumerable(同数据属性);
- get(获取函数);
- set(设置函数);
1-1-3、两种属性的区别
我们可以看一个例子
const a = {
name: 'xiaoming'
}
const b = Object.create(null);
Object.defineProperties(b, {
name: {
value: 'xiaohong'
},
age: {
get() {
return 21;
}
}
});
console.log(a);
console.log(b);
浏览器打印出来的结果如下:
有以下几处不同:
- b中的属性比a中的属性颜色浅一些。因为b中的属性是通过
Object.defineProperties
创建的,在没有指定configurable
、enumerable
、writable
的情况下,默认值为false
。不可枚举的属性(enumerable值为false),在浏览器里打印出来的颜色会以浅色区分。同样的,通过Object.defineProperty()
和Object.create(null, {...Properties})
创建的属性,也有这种特性。 - b中的age显示
(...)
。这代表是访问器属性,点击三个点,可以查看到它的值。(这种情况在vue中打印数据时,经常看到,因为这个属性是访问器属性,有get和set的方法) - a多了__proto__属性。因为b是通过
Object.create(null)
创建的,它的原型被指向null。我们通过对象字面量创建一个对象如:const a = {};
,它和const a = Object.create(Object.prototype);
可以认为是等价的。 需要注意的地方:
- 一个属性的configurable被设置为false之后,这个属性的相关特性(包括值)就再也无法修改了。无法删除,无法修改值,无法修改是否可枚举。也无法再把configurable修改为true。
- 依照enumerable的值,可以分为可枚举的属性(enumerable值为true),和不可枚举的属性(enumerable值为false)。
1-2、根据属性键分类
对象的属性只能为string和symbol两种类型。当然也可以设置一个number类型的键,如数组。但在取值的时候,依然可以通过这个number转化的string类型来取值。如const a =[1,4,5];
,通过a[1]
和a['1']
访问都是可以的。
依照这种规则,可以分为常规属性(键为string或者number类型),和符号属性(键为symbol类型)
2、遍历属性的方法列举
for in
,获取 实例 和 实例原型链 上,所有可枚举的 常规属性Object.keys()
获取实例的,所有可枚举的 常规属性Object.getOwnPropertyNames()
,获取实例上,所有的常规属性(包括可枚举的和不可枚举的)Object.getOwnPropertySymbols()
,获取实例上,所有的符号属性(包括可枚举的和不可枚举的)Object.getOwnPropertyDescriptors()
,获取实例上,所有的属性(包括可枚举的和不可枚举的,常规属性和符号属性),返回的是一个对象,含有属性的特性configurable、enumerable、writable等信息。Reflect.ownKeys
,和Object.getOwnPropertyDescriptors()
相同,但返回的是属性键名的数组,不含属性特性的信息。
另外,Object.getOwnPropertyDescriptor()
,可以获取实例上,单个特定属性的信息,用法如:Object.getOwnPropertyDescriptor(a, 'name')
。
需要注意的地方:
let a = {
name: 'xiaoming',
get age() {
return 12;
}
}
let b = {
name: 'xiaohong',
get age2() {
return 13;
}
}
b.__proto__ = a;
console.log(b);
console.log(Object.getOwnPropertyDescriptors(b));
打印结果如下:
在这里把a设置为b的原型,a中的访问器属性age
也会出现在b中age:(...)
,理解为访问器属性get age
是在原型a上的,但它的取值age,可以反应在继承了原型a的 b当中,但并不是真正的b实例上的属性。因为后边
Object.getOwnPropertyDescriptors(b)
方法获取b的直接的属性不能获取到访问器属性age
另外,如果:
let a = Object.create(null, { name: { value: 'xiaoming', } });
let b = Object.create(a, { name: { value: 'xiaohong', } });
会发现b.__proto__
值为undefined,这点和预期不符,暂时没找到原因。如果指定了a的原型,如Object.prototype
,则会符合预期。
3、写一个按照原型链,逐个获取对象属性的方法
3-1、获取原型链上的属性方法
如下:
function dealDesc(obj) {
const descObj = Object.getOwnPropertyDescriptors(obj);
//获得实例上的所有属性的集合,不包含原型上的属性
const temp = Object.create(null);
let arr = Reflect.ownKeys(descObj);
//可枚举的常规属性
temp['enumMost'] = arr.filter(v => descObj[v]['enumerable'] && typeof v !== 'symbol');
//可枚举的符号属性
temp['enumSymbol'] = arr.filter(v => descObj[v]['enumerable'] && typeof v === 'symbol');
//不可枚举的常规属性
temp['nonEnumMost'] = arr.filter(v => !descObj[v]['enumerable'] && typeof v !== 'symbol');
//不可枚举的符号属性
temp['nonEnumSymbol'] = arr.filter(v => !descObj[v]['enumerable'] && typeof v === 'symbol');
return temp;
}
function listProp(obj) {
const TEMP = Object.create(null);//创建一个空的对象
TEMP['self_prop'] = dealDesc(obj);//查找实例自己的属性
let proto = obj.__proto__, i = 1;
while (proto) {
let tag = proto[Symbol.toStringTag];
let _name = `proto_${i++}`
if(tag){
//有[Symbol.toStringTag]属性,则直接取这个值
_name += `_${tag}`;
}else if(Object.prototype.toString.call(proto) !== '[object Object]'){
//没有[Symbol.toStringTag],则调用toString获取
let tempStr = Object.prototype.toString.call(proto);
let str = tempStr.substr(0,tempStr.length -1).split(' ').pop()
_name += `_${str}`;
}else{
//没有[Symbol.toStringTag],
//且Object.prototype.toString.call(proto) === '[object Object]'的值不处理
}
//proto[Symbol.toStringTag]相当于
//Object.prototype.toString.call(proto)获得的值的"[Object object]"第二部分
if(proto === Object.prototype){
_name = 'Object.prototype'
}
TEMP[_name] = dealDesc(proto);
proto = proto.__proto__;
}
return TEMP;
}
3-2、[Symbol.toStringTag]
和Object.prototype.toString.call()
有以下需要注意的地方:
Object.prototype.toString.call()
是常用的区别对象类型的方法。但Object.prototype.toString.call()最先返回的是对象的[Symbol.toStringTag]
属性,如果没有这个属性,才会返回默认的值。如下:
let a = [1,2];
let b = new Set([1,2]);
console.log(Array.prototype[Symbol.toStringTag]);//undefined
console.log(Object.prototype.toString.call(a));//[object Array]
console.log(Set.prototype[Symbol.toStringTag]);//Set
console.log(Object.prototype.toString.call(b));//[object Set]
但如果我们手动修改了[Symbol.toStringTag]
,则会发生奇怪的现象:
let a = [1,2];
console.log(Array.prototype[Symbol.toStringTag]);//undefined
a[Symbol.toStringTag] = 'UUKK';
//或者Array.prototype[Symbol.toStringTag] = 'UUKK';
console.log(Array.prototype[Symbol.toStringTag]);//UUKK
console.log(Object.prototype.toString.call(a));//[object UUKK]
所以我们在上边的方法中,先判断有无[Symbol.toStringTag]
,如果有,则直接取这个值最为标识类型。如果没有再通过Object.prototype.toString.call()
获取标识类型。最终的原型Object.prototype
,则直接用'Object.prototype'标识。
我们执行上边的例子:
console.log(listProp(c));
会打印出如下内容:
其中self_prop代表对象c的直接的属性,enumMost、enumSymbol、nonEnumMost、nonEnumSymbol分别代表可枚举的常规属性、可枚举的符号属性、不可枚举的常规的属性、不可枚举的符号属性。proto_1是
c.__proto__
上的属性,proto_2是c.__proto__.__proto__
上的属性,依次类推。Object.prototype
也就是它的最顶层的原型(实际的原型终点为null)对象。可以看到其中有12个不可枚举的常规属性。
3-3、window
对象和[Symbol.iterator]
依照此方法,我们查询下window
和[]
:
console.log(listProp(window));
console.log(listProp([]));
发现:
可以看到window对象的原型链上有多个对象。另外数组自身有一个不可枚举的常规属性'length',
proto_1_Array
里边实际上就是Array.prototype
的属性集合。可以看到有一个[Symbol.iterator]
的不可枚举的符号属性,拥有这个属性,说明有对应的迭代器,所以它的实例可以使用for of来遍历。其他的如Map、Set、WeakMap、WeakSet,字符串的包装类型,nodeList对象和arguments对象都可以使用for of来遍历。
并且依靠这个方法,我们可以获得一个不确定的对象上的所有方法的列表,比如上边proto_1_Array
中可以查看到Array.prototype上有33个不可枚举的常规属性,和2个不可枚举的符号属性。
4、考虑到的深拷贝的问题
因为属性有configurable、 enumerable、writable、value、get、set这些特性。我们平常创建一个对象的时候,只指定value的值,其他的都是默认值。如果我们拷贝了一个设置有configurable:false的属性的对象时,会是什么样的效果呢?
4-1、对象的冻结
Object.preventExtensions()
、让一个对象变得不可扩展,不能再往里边添加新的属性,Object.isExtensible()
可以判断是否进行了不可扩展的操作;Object.seal()
、密封操作,不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值,Object.isSealed()
可以判断是否进行了密封操作;Object.freeze()
、冻结一个对象,不能在进行任何修改。但如果一个属性是一个引用类型,则可以修改这个引用类型里边的内容。因为Object.freeze()
可以理解为浅冻结。用递归调用Object.freeze()
可以把一个对象进行深层冻结。Object.isFrozen()
可以判断是否进行了冻结操作; 我们可以通过下边的例子看出区别:
const a = {
name: 'xiaoming'
},
b = {
name: 'xiaohong'
},
c = {
name: 'xiaohuang'
};
Object.preventExtensions(a);
Object.seal(b);
Object.freeze(c);
console.log(Object.getOwnPropertyDescriptors(a));
console.log(Object.getOwnPropertyDescriptors(b));
console.log(Object.getOwnPropertyDescriptors(c));
打印出
Object.preventExtensions()
是让对象无法添加新的属性,但对每个已有的属性不会有影响;Object.seal()
在Object.preventExtensions()
的基础上,会让每个属性变得无法再配置(configurable
改为false,这个过程不可逆);Object.freeze()
在Object.seal()
的基础上,把每个对象的变的不可修改(writable
改为false,因为同样修改了configurable
为false,所以也不可逆)。
4-2、模拟冻结
考虑到,Object.freeze()
操作会修改对象所有属性的configurable
和writable
,决定在创建对象时,指定这些属性的特性。看是否被认为是一个已经冻结的对象。如下:
const a = Object.create(null);
Object.defineProperty(a, 'name', {
enumerable: true,
value: 'xiaoming',
});
console.log(a);
console.log(Object.getOwnPropertyDescriptors(a));
console.log(Object.isFrozen(a));
打印的结果如下:
可见这样的创建方式欺骗不了程序,不被认为是一个已经冻结的对象。 后边仔细一想,还缺少不可添加新属性的要求,所以,确实和真实的冻结还有区别。
4-3.深拷贝遇到冻结对象
一个简易的深拷贝,处理数组和普通对象:
const deepClone = function f(obj, map = new Map()) {
if (typeof obj === 'object' && obj !== null) {
if (map.has(obj)) {
return map.get(obj)
}
let temp = Array.isArray(obj) ? [] : {};
map.set(obj, temp);
for(let key of Object.keys(obj)){
temp[key] = f(obj[key], map)
}
return temp;
} else {
return obj;
}
}
执行以下操作:
const a = Object.create(null);
Object.defineProperties(a, {
'name': {
configurable: true,
enumerable: true,
writable: true,
value: 'xiaoming',
},
'age': {
configurable: true,
enumerable: true,
get() {
return 21;
}
}
});
Object.freeze(a);//冻结
const b = deepClone(a);//深拷贝
console.log(a);
console.log(Object.getOwnPropertyDescriptors(a));
console.log(b);
console.log(Object.getOwnPropertyDescriptors(b));
打印出以下内容:
从打印内容可以看出,不做特殊处理的情况下,拷贝一个冻结的对象,它的configurable
、writable
会被修改为true,并且访问器属性会被修改为默认的数据属性。
要做到正确拷贝冻结的对象,一个思路是判断下Object.isFrozen()
的值,根绝这个值,来决定是否对这个对象冻结处理。另外还要通过Object.getOwnPropertyDescriptors()
获取每个属性的相关特性,设置成一样的configurable
、writable
、enumerable
、value
和get
、set
。这些都是深拷贝需要考虑到的情况。
5、收尾
以上提供了深拷贝遇到冻结对象,特定configurable
、writable
、enumerable
值的对象时的拷贝方法。但还有访问器属性的拷贝问题,后边再做学习。