js中微妙的对象属性

117 阅读9分钟

JS中微妙的对象属性

1.对象的自有属性的属性和Object.defineProperty(target,key,option)添加的属性有什么区别?

const obj = { name:'bob' };
Object.defineProperty(person,'age',{ value:12 });
console.log(obj.age);//?12
console.log(Object.keys(obj));//?['name']
console.log(Object.getOwnPropertyNames(obj))//?['name','age']
/*
	大多数人认识Object.defineProperty()这个API
	是因为Vue2,使用Vue2的工程师细心的应该注意过一个问题:
	为什么输出的响应式对象需要点开才能看到属性,这是为什么?
	如上,我们使用Object.defineProperty()为对象添加了属性
	value,其余属性没有定义,它们的值是什么?
	直接量字面量创建的对象的属性的默认的属性描述符是什么样的?
*/

//1)对象的自有属性的属性描述符
const nameDescriptor = Object.getOwnPropertyDescriptor(obj,'name')
//{value: 'bob', writable: true, enumerable: true, configurable: true}
console.log(nameDescriptor)
//2)通过Object.definePropery()添加的属性不设置属性描述符时的实际属性描述符的情况
const ageDescriptor = Object.getOwnPropertyDescriptor(obj,'age')
console.log(ageDescriptor)
//{value: 123, writable: false, enumerable: false, configurable: false}

/*
	对象中的属性描述符有:
	enumerable:boolean,可枚举性,默认true;
	configurable:boolean,配置性,默认true;
	value:any,属性的初始值;
	writable:boolean,是否可写默认值是true
	
	除此之外还有大家熟知的getter和setter
	这两个和上面的属性描述符是冲突的,所以有
	getter/setter就不要写以上的属性描述符
	
	上面出现了惊人的场景:虽然我们没有设置属性描述符
	但是使用Object.defineProperty()定义的属性
	属性描述符是false
	所以上面的结果比较出乎意料,下面判断对象是否为空对象的案例也与此有关
*/

/*
	以上用到几个API,简单介绍一下:
		1.Object.keys(object)
			和Object.values(object)方法类似
			接收一个对象作为参数,返回对象的可枚举属性
			enumerable为true的key组成的数组和可枚举属性
			的value组成的数组,只找对象自有属性属性值,不会
			去对象的原型对象上找。
			特别强调:必须是可枚举属性
		2.Object.getOwnPropertyNames()接收对象参数,返回
			对象自有属性的可枚举不可枚举的key组成的数组,
			只找对象的自有属性
			不会去对象的原型对象上找。
			特别强调:无论属性是否可枚举都会获取到
		3.Object.getOwnPropertyDescriptor(object,key)
			获取对象的某个属性的属性描述符对象
*/

2.如何判断一个对象是否为空对象?

​ 这是一个老生常谈的问题,由于object对象是引用数据类型,存储于计算机内存的堆内存中 ​ 通过指针访问,所以对象是否为空对象的判断不能使用 obj === {} 或者 obj == {} ​ 网上流行的方式: ​ 1.JSON.stringify(object) === '{}' ​ 2.for in循环方式 ​ 3.Object.keys(object) ​ 4.Object.getOwnPropertyNames(object)

​ 我们逐一测试:

//方式1:JSON.stringify(object) === "{}"判断是否为true
	const object = { name : 'Andy' },
          emptyObj = {};
	let result = JSON.stringify(emptyObj) === "{}",
    	result2 = JSON.stringify(object) === '{}'
    console.log(result)//true
    console.log(result2)//false
    
	/*
		如果对象真的是空对象,那JSON.stringfiy(object)
		的判断结果没问题,但是当对象里只有方法呢?
	*/
	const object2 = { func(name){ console.log(name) } };
	const result3 = JSON.stringify(object2) === '{}';
     console.log(result3)//true
	/*
		对象中的方法比较特殊,本质还是对象的属性,只不过值是函数
		被称为对象的方法,所以 JSON.stringify() 的第一个坑出现
		JSON.stringify()不能转化对象的方法,或者说它会自动忽略
		对象中的方法,导致判断失效,其实至此,JSON.stringify()
		判断空对象已经失效了
	*/
	//测试JSON.stringify()转化circular对象
	const object3 = { name:'object3' };
	object3.a = 1;
	object3.self = object3
	const result4 = JSON.stringify(object3) === '{}'//error
    /* 
    	Uncaught TypeError: Converting circular structure to JSON
    	--> starting at object with constructor 'Object'
    	--- property 'self' closes the circle
    */
 
    /*
    	circular对象:对象中的某个属性值是对象自身
    	简单说就是可以 '无限循环点开' 的对象
    	很遗憾,JSON.stringify()不能转化circular对象
    	所以目前发现JSON.stringify()转化对象的两个
    */
    /*
    	结论:判断对象是否为空对象请彻底pass JSON.stringify()方法
    */
	
//方式2:for in方式

	/*
		看到网上有一种方式是封装for in方法,
		如果进入对象的循环体则表示不是空对象,
		否则是空对象,那看过上面的阐述应该能
		想到对象的enumerable属性对循环遍历的影响
	*/

	function isEmpty(object){
        /*前置判断略过*/
        for(const key in object){
            return false
        }
        return false
    };
  /*
  	首先它会有一个致命问题是,当对象里的所有属性不可枚举时
  	这个方法会失效,演示过程略过,它和上面Object.keys()
  	无法获取不可枚举属性的原理是一致的
  */

	/*
	然后我们考虑一种情况:for in 是否会遍历对象的原型对象上的
	属性呢?
	根据以往的经历:我们for in 一个直接量字面量创建的对象时,它
	并没有枚举Object.prototype上的属性方法,因为它们默认是不可枚举的。
	有看官大佬应该已经想到了:Object.create(prototype:object,option?:object)
	*/

	const object = Object.create({ property1 : 'props1' });
    console.log(object);//{}
    console.log(object.__proto__)//{ property1 : 'props1' }
	const result = isEmpty(object)
    console.log(result)//false
	/*
		我们发现for in 遍历了使用Object.create()方法创建的空对象
		以及空对象的原型对象,所以for in 方法失效
	*/
	/*
		Object.create()方法接收两个参数,第一个参数是要创建的对象的原型对象
		第二个参数也是对象,是可选参数,是新对象的访问器属性集合,是属性描述符对象
		我们简易的模拟一下Object.create()方法更直观:
	*/
        function createObject(proto, config) {
			/* 前置判断略过 */
            const object = {}
            Object.setPrototypeOf(obj, proto)
            if (config && Object.getOwnPropertyNames(config).length > 0) {
                for (const key in config) {
                    if (Object.prototype.hasOwnProperty.call(config, key)) {
                        const element = config[key];
                        Object.defineProperty(obj, key, element)
                    }
                }
            }
            return object
        }
        const originObj = { a: 12, b: '213' };
        const handler = {
            name: {
                enumerable: false,//不可枚举
                configurable: true,
                writeable: true,
                value: '张三'
            }
         }
        const result1 = createObject(originObject,handler)
        console.log(result1)//{name: '张三'} __proto__ { a: 12, b: '213' }
	    console.log(result1.__proto__)//{a: 12, b: '213'}
		console.log(Object.getOwnPropertyNames(result1))//['name'];不管可不可枚举都能拿到
	    console.log(Object.keys(result1))//[],name属性确实是不可枚举的
		
	/*
		分析一下上述代码中的API:
			1. Object.setPrototypeOf(target,__proto__)
				设置一个对象的原型对象,
			2.与之相对应的Object.getPropertyOf(target)
				获取一个对象的原型对象
				之所以要提一下这个没用到的API,是因为__proto__
				已经被废弃了,不过ES目前是强制要求各大浏览器还可以通过
				__proto__获取到实例对象的隐式原型,并且ES也提供了
				两个获取设置修改对象的原型对象的API
				
			3.Object.prototype.hasOwnProperty.call(this, key)
				这种修改this指向的方式既冗长又不美观,难道程序员
				就为了凑字?
				这种写法的原因1:
				目标对象没有这个方法,还想用这个方法,比如:
				//在函数体中:
				var arg = [].slice.call(arguments)
				arguments是个类数组对象,我们想将其快速转为数组,这在ES5中
				很常见.
				Object.prototype.hasOwnProperty(key)方法确实是个成员方法
				但是hasOwnProperty并不是关键字,这意味着你可以直接量字面量
				创建一个包含hasOwnProperty方法的对象,此时object.hasOwnProperty(key)
				默认访问的是对象自身的,而不是原型对象上的hasOwnProperty(原型链机制)
				
				所以VS Code中生成的代码中有这种修改this指向的用法,这能有效提升
				代码的严谨性
				
	*/

		
//方式3: Object.keys(object)

基于上述的代码示例,Object.keys()会在对象自身找可枚举属性组成数组,所以这个方法不严谨
//方式4:Object.hasOwnPropertyNames()

目前我所知的最好的最简洁的判断对象是否为空对象

至此,据我所知,判断对象是否为空对象只有最后这种方式比较严谨

3.常见场景之判断一个对象是否存在某个属性,后执行对应操作

const object = { age : 12 };
//如果object存在name属性,控制台打印name的值
if(object.name){
    console.log(object.name)
}

/*
	这看起来没有什么问题,很符合我们的编程习惯,但是如果
	我执行了如下代码,再执行上面的if判断呢?
*/
object.name = undefined;
object.position = null;
if(object.name) console.log(object.name)
else console.log('undefineds')//'undefineds'

/*
	首先说对象中是可以存储任意值的,
	声明变量不要显式赋值为undefined,但是可以赋值为null
	而对象的属性可以赋值为任意值,
	如果访问对象不存在的属性(原型链上也不存在)时会得到undefined
	但是一个对象的某个属性就是undefined,这时判断就可能出问题:
	我们发现上面走了else 分支
	object.name不存在得到undefined是假值
	object有name属性但是值是undefined是假值
	
*/
if(object.name) console.log(object.name)
else console.log('undefineds')//'undefineds'

if(Object.prototype.hasOwnProperty.call(object,'name')) 						console.log(object.name)//undefined
else console.log('undefineds')
/*
	确定是要判断一个对象的属性是否存在还是判断一个对象
	的某个属性值是否是真值是非常值得思考的问题,
	hasOwnProperty(key)方法返回布尔值,判断一个对象是否
	有某个自有属性
*/

4.使用对象的计算属性属性优化if...else分支语句

/*
	打个预防针:不要一提到计算属性就是Vue中的computed计算属性
	其实还有一种说法:object[variable] = value,传入变量动态获取
	值,考虑以下代码:
*/

function getVal (key){
    const object = { a:1,b:2,c:3 };
    if(key === 'a'){
        return object.a
    }else if(key === 'b'){
        return object.b
    }else if(key === 'c'){
        return object.c
    }
    throw new Error(key + 'doesn`t exist in object')
}
/*
	if...else...分支语句本身条理清晰,但是条件过多会显得代码
	冗余,当然将if...else...转换成Switch也是不错的选择,
	有道是程序员都是懒人,所以我们应该在保证功能的前提下
	尽可能精简代码,所以用对象的计算属性试试看:
*/

function getValue (key) {
    const object = { a:1,b:2,c:3 };
    if(Object.prototype.hasOwnProperty.call(object, key)){
        return object[key]
    }
    throw new Error(key + 'doesn`t exist in object')
}

//其实还可以使用其他工具优化,比如数组方法:
function getValues(key){
    const object = { a:1,b:2,c:3 };
    const keyArray = Object.getOwnPropertyNames(object)
    if(keyArray.includes(key)) return object[key]
    throw new Error(key + 'doesn`t exist in object')
}

5.目前js中对象的键值的合法值都有哪些数据类型?

/*
	乍一聊起这个问题很多人还是有些犯迷糊的,先说结论:
		1.string,这个不用说都知道
		2.number,好像也行吧?
		3.Symbol,Symbol是什么鬼?
*/
const object = { a:1, b:2, c:3}
object[1] = 10
object[Symbol('ES')] = 20
console.log(object)//{1: 10, a: 1, b: 2, c: 3, Symbol(ES): 20}
console.log(Symbol('ES') == Symbol('ES'))//false
console.log(Symbol('ES') === Symbol('ES'))//false
/*
  Symbol(description)
  即便传入了相同的description也永远不会相等也不全等
  数值作为对象合法的键值也就解释了为什么说数组是特殊的对象:
  数组可以看作键值为依次递增的对象
  而Symbol()方法会创造出唯一的值,它没有字面量表示方式,
  但是可以转存于一个变量中,放在公共位置避免重复
  比较典型的场景是Vue中需要Provide(key,data)inject(key)
  时可以将一个Symbol()值存放在一个公共的位置,组件引入并使用
  它
*/

说了很多但是其实内容没有多少,但是比较细节,纯手码,也是想到哪写到哪,今天暂时告一段落

欢迎大佬斧正,欢迎志同道合的朋友提出宝贵看法意见,成长的道路有你也有我!