属性描述符defineProperty
属性描述符是会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
数据描述符
可以使用Object.getOwnPropertyDescriptor(obj,key)
来获取obj对象内的数据描述符。具体有以下四种:
- value,用于设置该属性的值
- writable,用于设置该属性可否重写
- enumerable,用于设置该属性是否可遍历
- configurable,用于设置该属性描述符本身是否可修改
下面给出一些案例,我们准备一个商品的对象,属性有title和price:
let GoodsObj = {
title: '商品1',
price: 3,
}
- 查看该商品的属性描述符:
const desc=Object.getOwnPropertyDescriptor(GoodsObj,'price')
console.log(desc)
//{value: 3, writable: true, enumerable: true, configurable: true}
- 验证value 修改该对象的价格:
Object.defineProperty(GoodsObj,'price',{
value:4,//值
})
//{title: '商品1', price: 4}
- 验证writable 将价格的writable设置为false(不可修改),然后将价格修改为10,通过比哪里的方式将GoodsObj展示出来,发现价格属性确实不能进行后续的修改操作
Object.defineProperty(GoodsObj,'price',{
writable: false,//不可重写
})
GoodsObj.price=10
for (const key in GoodsObj) {
if (Object.hasOwnProperty.call(GoodsObj, key)) {
const element = GoodsObj[key];
console.log(key+':'+element)
}
}
//title:商品1
//price:4
- 验证enumerbale 先将GoodsObj进行遍历,然后将价格属性的enumrable设置为false(不可遍历),再将GoodsObj进行遍历
for (const key in GoodsObj) {
if (Object.hasOwnProperty.call(GoodsObj, key)) {
const element = GoodsObj[key];
console.log(key+':'+element)
}
}
Object.defineProperty(GoodsObj,'price',{
enumerable: false,//不可遍历
// configurable:false//不可修改属性描述符本身
})
for (const key in GoodsObj) {
if (Object.hasOwnProperty.call(GoodsObj, key)) {
const element = GoodsObj[key];
console.log(key+':'+element)
}
}
/*
第一次遍历
title:商品1
price:3
第二次遍历
title:商品1
*/
- 验证configurable,将GoodsObj的价格属性的configurable设置为false,writable设置为false,在尝试将writable设置为true,运行后会报错
Object.defineProperty(GoodsObj,'price',{
writable:false,
configurable:false//不可修改属性描述符本身
})
GoodsObj.price=10
//价格无法修改,还是初始值3
Object.defineProperty(GoodsObj,'price',{
writable:true
})
//报错:Uncaught TypeError TypeError: Cannot redefine property: price
补充:在将configurable设置为false之后,是不可以在将configurable设置为true的,这是一个单向的设置,并且也无法将其他属性从false设置为true(value除外),但是可以将其他属性从true设置为false
//第一种情况
Object.defineProperty(GoodsObj,'price',{
configurable:false//不可修改属性描述符本身
})
Object.defineProperty(GoodsObj,'price',{
configurable:true
})
//报错:Uncaught TypeError TypeError: Cannot redefine property: price
//第二种情况
Object.defineProperty(GoodsObj,'price',{
writable:false,
configurable:false//不可修改属性描述符本身
})
Object.defineProperty(GoodsObj,'price',{
writable:true
})
//报错:Uncaught TypeError TypeError: Cannot redefine property: price
Object.defineProperty(GoodsObj,'price',{
writable:true,
configurable:false//不可修改属性描述符本身
})
GoodsObj.price=10
//{title: '商品1', price: 10}
Object.defineProperty(GoodsObj,'price',{
writable:false
})
GoodsObj.price=3
//{title: '商品1', price: 10}
存取描述符
- 读取器get: 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。
- 存取器set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined。
这有什么用呢?仍以上面的GoodsObj数据为例,现在有一个Goods类,里面可以存放GoodsObj的信息,也有choose选中数量,isChoose是否被选中了以及计算总价,这是一个很简单的类,我们可以写出如下代码:
class Goods {
constructor(g) {
this.data = g
this.choose = 0
this.isChoose = flase
}
totalPrice() {
return this.choose * this.data.price
}
}
乍一看好像觉得蛮正常,但是仔细一想,这个类是有很大的问题的,例如我们进行如下操作:
let g = new Goods(GoodsObj)
g.data='恶意修改',
g.choose=-20
g.isChoose='没选'
g.totalPrice=1
console.log(g)
//Goods {data: '恶意修改', choose: -20, isChoose: '没选', totalPrice: 1}
当我们修改了g实例的属性后,发现g商品的所有信息都出错了,在实际开发中是不允许的。这时就需要使用到读取器和存取器,我们可以将构造器中的data改成这样:
···
constructor(g) {
Object.defineProperty(this, 'data', {
get() {
return g
},
set(value) {
throw new Error('data只读,无法重新赋值')
},
configurable: false
})
}
···
let g = new Goods(GoodsObj)
g.data='恶意修改'
console.log(g)
//Uncaught Error Error: data只读,无法重新赋值
这样data数据就无法进行修改,只能进行读取,我们也可以对choose进行同样的操作,并且由于set是一个函数,我们可以在函数里面进行类型限制
//由于构造器中没有初始值,并没每次访问choose不可能都为0,所以我们自己设置一个internalChoose用于记录选中多少件商品,它不能被类外访问到
let internalChoose = 0
Object.defineProperty(this, 'choose', {
configurable: false,
get() {
return internalChoose
},
set(val) {
if (typeof val !== 'number') {
throw new Error('choose的值只能为数字')
}
if (val !== parseInt(val)) {
throw new Error('choose的值只能为整数')
}
if (val < 0) {
throw new Error('choose的值必须大于等于0')
}
internalChoose = val
}
})
并且get和set也能写在构造器外,直接写在Goods类下面
class Goods {
get totalPrice() {
return this.choose * this.data.price
}
get isChoose() {
return this.choose > 0
}
····
}
但是此时还有一些问题,例如data不能修改了,但是data内的price还是可以被外部修改
···
g.data.price='nihao'
console.log(g.data.price)
//nihao
我们不能直接冻结构造器内的对象,可以克隆一个对象,然后冻结克隆对象来解决这个问题
constructor(g) {
····
//冻结克隆对象,原始对象可以修改
g = { ...g }
Object.freeze(g)
····
}
····
g.data.price='nihao'
console.log(g.data.price)
//3
还有一个问题,我们还可以往g实例中添加新的属性
g.name='111'
console.log(g)
/Goods {name: '111', data: <accessor>, choose: <accessor>}
这时候我们可以使用seal封装,在构造器最后使用seal,就可以保证不能新增属性,只能修改现有属性。
完整代码如下:
let GoodsObj = {
title: '商品1',
price: 3,
}
class Goods {
get totalPrice() {
return this.choose * this.data.price
}
get isChoose() {
return this.choose > 0
}
constructor(g) {
//冻结克隆对象,原始对象可以修改
g = { ...g }
Object.freeze(g)
Object.defineProperty(this, 'data', {
get() {
return g
},
set(value) {
throw new Error('data只读,无法重新赋值')
},
configurable: false
})
let internalChoose = 0
Object.defineProperty(this, 'choose', {
configurable: false,
get() {
return internalChoose
},
set(val) {
//条件限制
if (typeof val !== 'number') {
throw new Error('choose的值只能为数字')
}
if (val !== parseInt(val)) {
throw new Error('choose的值只能为整数')
}
if (val < 0) {
throw new Error('choose的值必须大于等于0')
}
internalChoose = val
}
})
//不能新增属性,可以修改现有属性
Object.seal(this)
}
}
let g = new Goods(GoodsObj)