遍历对象方法总结及思考

705 阅读9分钟

1、对象属性的分类

1-1、数据属性和访问器属性

1-1-1. 数据属性的特性:

  1. configurable(是否可以修改属性的特性,是否可以删除属性);
  2. enumerable(是否可枚举,是否可以通过 for-in 循环返回);
  3. writable(属性的值是否可以被修改);
  4. value(属性值);

1-1-2. 访问器属性的特性:

  1. configurable(同数据属性);
  2. enumerable(同数据属性);
  3. get(获取函数);
  4. 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);

浏览器打印出来的结果如下:

image.png

有以下几处不同:

  1. b中的属性比a中的属性颜色浅一些。因为b中的属性是通过Object.defineProperties创建的,在没有指定configurableenumerablewritable的情况下,默认值为false。不可枚举的属性(enumerable值为false),在浏览器里打印出来的颜色会以浅色区分。同样的,通过Object.defineProperty()Object.create(null, {...Properties})创建的属性,也有这种特性。
  2. b中的age显示(...)。这代表是访问器属性,点击三个点,可以查看到它的值。(这种情况在vue中打印数据时,经常看到,因为这个属性是访问器属性,有get和set的方法)
  3. 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));

打印结果如下: image.png

在这里把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));

会打印出如下内容:

image.png 其中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([]));

发现:

image.png 可以看到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));

打印出

image.png

Object.preventExtensions()是让对象无法添加新的属性,但对每个已有的属性不会有影响;Object.seal()Object.preventExtensions()的基础上,会让每个属性变得无法再配置(configurable改为false,这个过程不可逆);Object.freeze()Object.seal()的基础上,把每个对象的变的不可修改(writable改为false,因为同样修改了configurable为false,所以也不可逆)。

4-2、模拟冻结

考虑到,Object.freeze()操作会修改对象所有属性的configurablewritable,决定在创建对象时,指定这些属性的特性。看是否被认为是一个已经冻结的对象。如下:

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));

打印的结果如下:

image.png

可见这样的创建方式欺骗不了程序,不被认为是一个已经冻结的对象。 后边仔细一想,还缺少不可添加新属性的要求,所以,确实和真实的冻结还有区别。

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));

打印出以下内容:

image.png

从打印内容可以看出,不做特殊处理的情况下,拷贝一个冻结的对象,它的configurablewritable会被修改为true,并且访问器属性会被修改为默认的数据属性

要做到正确拷贝冻结的对象,一个思路是判断下Object.isFrozen()的值,根绝这个值,来决定是否对这个对象冻结处理。另外还要通过Object.getOwnPropertyDescriptors()获取每个属性的相关特性,设置成一样的configurablewritableenumerablevaluegetset。这些都是深拷贝需要考虑到的情况。

5、收尾

以上提供了深拷贝遇到冻结对象,特定configurablewritableenumerable值的对象时的拷贝方法。但还有访问器属性的拷贝问题,后边再做学习。