JavaScript基础(4)—— 包罗万象的对象(下)

  上一章主要了解了对象的增删改查,引出了原型链和继承的概念,本章我们来好好聊一聊对象的属性,在对象的属性中有一些比较好用的方法和特性,了解这些方法可能对你写代码不会有什么帮助,但如果你要封装一个大型的,好用的对象——比如Vue,你可能就需要用到里面的许多工具了。

1.检测属性

有时候我们需要检测一个对象中是否包含某个属性,这个时候我们可以通过in运算符、hasOwnPreperty()或propertyIsEnumerable()方法来完成这个工作,当然你也可以通过点(.)或方括号([])运算符查询来做到这一点。下面我们通过简单的代码来回顾下这几种检测方式。

let o = {x:1}
o.y === undefined // true 最简单的属性查询方式
'x' in o // true 注意要用引号把属性值包起来
'toString' in o // true in运算符可以查询到继承属性
o.toString() // '[Object Object]' 直接查询也可以查询到继承属性

o.hasOwnProperty('toString') 
// false hasOwnProperty用来检测对象的自有属性,对于继承属性返回false

o.propertyIsEnumerable('toString') 
// false propertyIsEnumerable是hasOwnProperty的增强版
// 只有检测到可枚举型的自有属性为true时返回true。通常由JavaScript创建的对象默认是可枚举的。

  需要注意的时,hasOwnProperty()方法只能检测到对象的自有属性,这看起来好像有点鸡肋,但某些情况下这个方法十分有用,当某个对象继承自一个十分庞大的父对象时,你可能不完全清楚继承来的有哪些属性值,这时候你想访问该对象的某个属性又不想受到父对象干扰的时候就可以用这个方法检测该对象的自有属性。关于propertyIsEnumerble方法,人如其名,该方法检测的属性不仅是自有属性,还得是可枚举的,关于可枚举属性我们会在后面有所介绍。

2.枚举属性

  除了检测对象是否存在,我们还需要检测对象是否可枚举。当我们用for / in 循环遍历对象的属性时,不但可以遍历对象的自有属性,还可以遍历对象继承的属性,我们在使用for / in的时候基本上都只遍历到了自己想要的属性,说明Object.prototype里面的属性都是不可枚举的,这点需要注意一下,下面来看一个例子,来证明下for / in 不但可以遍历自有属性还可以遍历继承属性。

// 继承 这个方法在上一章已经写过,这里再回顾一下
function inherit(p){
    if(p===null){
		throw TypeError()
	}
	if(Object.create){
		return Object.create(p)
	}
	if(typeof(p)!=='object'&&typeof(p)!=='function'){
		throw TypeError()
	}
	function fn(){}
	fn.prototype = p
	return new fn()
}

let o = {x:1}
let p = inherit(o)
p.y = 2
for(let key in p){
	console.log(key) //第一次是自有属性y,第二次是继承的可枚举属性x
}

  由于for/in循环有时候会遍历到一些我们不想要的属性(如从某个菜逼写的构造函数里继承的一些可枚举属性),因此我们需要对这些属性进行过滤,得到我们想要的自有属性。事实上ES5提供的Object.keys()方法已经帮我们过滤了继承的可枚举属性,实现的思路如下所示

Object.prototype.keys= function(o){
  if(typeof(o)!=='object') {throw TypeError()}
  let	arr = [] //用与存储key值
	for(let prop in o){
		if(o.hasOwnProperty(prop)){
			arr.push(prop)
		}
	}
	return arr
}

let o = {x:1}
let p = inherit(o)
p.y = 2
Object.keys(p) // ['y']

3.属性的监听器getter和setter

  我们都知道Vue数据双向绑定的核心就是改写对象属性的get和set方法,在ES5种,属性值可以用一个或者两个方法代替,这两个方法就是getter和setter,由getter和setter定义的属性称作“存储器属性”,我个人更喜欢把他称作“被监听属性”,既然有了“存储器属性”的官方说法,那么普通的属性我们称之为数据属性。

  当程序查询一个“被监听属性”(在后面我都会用被监听属性代替存储器属性的官方说法)时,JavaScript就会去调用该属性的getter方法,注意这个方法是没有参数的,这个方法的返回值就是被监听属性的值。当程序设置(改写)一个被监听的属性的值的时候,JavaScript就会调用该属性的setter方法,并把赋值表达式右侧的值当作参数传入setter。

  总而言之,被监听属性可以人为改写读取和设置属性时的行为,这样我们可以在读取和设置属性的时候对属性以及和属性相关的操作为所欲为之为所欲为,有了这两个方法,我们就可以实时监听数据的变化和读取了。如果属性同时具有getter和setter方法,那么他具有读/写属性,如果他只有getter方法,那么他是一个只读属性,如果他只有setter方法,那么他是一个只写属性,读取只写属性永远返回undefined。

  说了这么多,下面我们来简单使用下属性的getter和setter方法做一些为所欲为的操作

var rect = {
	'width':3,
	'height':4,
	// 读取对角线
	get diagonal(){
		return Math.sqrt(this.width*this.width + this.height*this.height)
	},
	// 改对角线
	set diagonal(newVal){
		var oldVal = Math.sqrt(this.width*this.width + this.height*this.height)
		var ratio = newVal/oldVal
		this.width *= ratio
		this.height *= ratio
	},
	// theta只读
	get theta(){return Math.atan2(this.height,this.width)}
}
rect.diagonal // 5
rect.diagonal = 10
rect.width //6
rect.height //8

  上面这段代码里的this关键字都指向当前对象,你只需要知道这一点就可以了,当我们访问对角线属性的时候,返回的是一个表达式的计算值,当我们改写对角线的时候,他的宽高信息也会做出相应的变化。通过属性的getter和setter方法对数据进行监听在许多场景中都有实际应用,这里不过多介绍了(因为本章的内容实在是有点多!)

4.属性的三大特性

  对象的属性除了key和value之外,还包含了一些标识他们是否可写,可枚举和可配置的特性,你也可以称之为属性的三大状态值。这些特性值在所有的属性中都存在,但只有通过特殊的API才能进行设置,了解这些API对开发业务并没有什么帮助,但如果你想开发一个API库,这些方法就显得格外重要了。

  还记得我们在将for/in枚举属性的时候,继承属性会被for/in循环打印出来的问题吗?你肯定不能指望使用你构建的API工具的用户还知道如何使用hasOwnProperty()吧,因此你需要将这些可能被继承的属性和方法设置成不可枚举的,这可以让他们看起来更像是一个内置方法。你还可以通过将对象的属性设置成不可写的来锁定这个属性的值。

  JavaScript把数据属性的值(value)和被监听属性的get,set方法作为属性的特性,以此将他们分为

  数据属性的四大特性:值(value),可写性(writable),可枚举型(enumerable)和可配置性(configurable)

  由于被监听属性的读写性由get和set决定。

  被监听属性的四大特性:可读性(get),可写性(set),可枚举型(enumerable)和可配置性(configurable)

  介绍完了属性的特性,那么JavaScript提供了哪些API来操作属性的特性呢?首先ES5定义了一个名为“属性描述符”的对象,用于查询某个对象的某个属性的属性描述符,你可以通过调用Object.getOwnPropertyDescriptor(obj,key)进行查询。

Object.getOwnPropertyDescriptor({x:1},'x')
//{value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(rect,'diagonal')
//{get: ƒ, set: ƒ, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({x:1},'toString')
//undefined

  需要注意的是,Object.getOwnPropertyDescriptor()方法只能得到自有属性的描述符,想要获得继承属性的特性,需要遍历原型链,这在后面会讲到如何实现。

  说完了如何查询属性的特性,我们再来聊聊如何设置属性的特性。调用Object.defineProperty()就可以实现对象某个属性的设置。下面来看一个简单的示例:

let o = {}
Object.defineProperty(o,'x',{
	value:1,
	writable:true,
	enumerable:false, //设置x属性不可枚举
	configurable:true
})
o.x // 1
Object.keys(o) // [] x属性不可枚举

//下面我们对属性x做修改,让他变为只读属性
Object.defineProperty(o,'x',{writable:false})

//试图修改这个属性的值
o.x = 2 //操作失败但不报错,严格模式下会抛出类型错误异常
o.x //1

  在上述例子中,我们设置了x属性不可枚举且不可写,那么我们是不是没有办法修改x的值了呐?并不是!由于x属性依然是可配置的,我们还是可以调用Object.defineProperty方法修改他的读取值。方法如下

Object.defineProperty(o,'x',{value:2})
o.x //2

  从上面的写法可以看出,传入Object.defineProperty的属性描述符对象不必包含所有四个特性。还需要注意一个点,该方法只能修改或新建自有属性,不能操作继承的属性。

  如果需要同时修改或者创建多个属性,我们可以使用Object.defineProperties(),第一个参数是要修改的对象,第二个参数是一个映射表,示例如下:

var p = Object.defineProperties({},{
    x:{value:1,writable:true,enumerable:true,confitable:true},
    y:{value:2,writable:true,enumerable:true,confitable:true}
    ...
})

  本章主要介绍了对象属性的一些操作方法,关于对象的三个属性(原型,类型,可扩展性),可以了解一下,关于对象的方法,比较常用的对象序列化操作可以了解一下,本章有时间的话,我会补一下对象的原型属性,其他内容就不再多说了,多说无益,感兴趣的可以点个关注,也可以入驻个人粉丝群708637831

发布了109 篇原创文章 · 获赞 196 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/dkr380205984/article/details/99567693