属性描述符defineProperty

属性描述符是会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。属性描述符有两种主要形式:数据描述符存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

数据描述符

可以使用Object.getOwnPropertyDescriptor(obj,key)来获取obj对象内的数据描述符。具体有以下四种:

  1. value,用于设置该属性的值
  2. writable,用于设置该属性可否重写
  3. enumerable,用于设置该属性是否可遍历
  4. configurable,用于设置该属性描述符本身是否可修改
    下面给出一些案例,我们准备一个商品的对象,属性有title和price:
let GoodsObj = {
    title: '商品1',
    price: 3,

}
  1. 查看该商品的属性描述符:
const desc=Object.getOwnPropertyDescriptor(GoodsObj,'price')
console.log(desc)
//{value: 3, writable: true, enumerable: true, configurable: true}
  1. 验证value 修改该对象的价格:
Object.defineProperty(GoodsObj,'price',{
    value:4,//值
})
//{title: '商品1', price: 4}
  1. 验证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
  1. 验证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
*/
  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}

存取描述符

  1. 读取器get: 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。
  2. 存取器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)
posted @ 2023-03-18 14:17  超重了  阅读(46)  评论(0编辑  收藏  举报