释义

JS中的属性描述符描述了一个对象中属性的一些特性,包括值、可被遍历、可被修改等属性,为了更好的演示属性描符的特点,我们先定义如下两个对象

//手机产品数据
const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}
//定义一个类,表示产品的购买手机信息
class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

首先,我们来看一下属性描述符是什么,它通过ObjectgetOwnPropertyDescriptor方法来获取指定对象指定属性的属性描述符

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

const uiProduct = new UIProduct(product);
console.log(Object.getOwnPropertyDescriptor(uiProduct, 'data'));

打印结果如下

image-20230801174248076

可以直观的看到,属性描述符实际上就是一个对象,包含了以下几个属性

属性名 描述
value 属性的值
writable 属性是否可以被重写
enumerable 属性是否可以被迭代
configurable 属性描述符是否可以被修改

因此,我们可以通过修改属性描述符的方法来修改对象的属性,修改对象属性的属性描述符使用ObjectdefineProperty方法来进行修改,接下来我们对属性描述符的属性进行逐个分析。

修改value

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

console.log(`修改前:${product.name}`);
Object.defineProperty(product,'name',{
    value: 'Xiaomi 12Pro'
})
console.log(`修改后:${product.name}`);

image-20230801175653557

可以看到通过属性描述符可以不通过赋值的方式来修改某个对象的某个属性。

修改writable

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

console.log(`修改前:${product.name}`);
Object.defineProperty(product,'name',{
    writable: false
});
product.name = 'Xiaomi 11';
console.log(`修改后:${product.name}`);

image-20230801192840687

将对象属性描述符的writable修改为false后,对象的的值将不能再被修改。

修改enumerable

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

console.log(`修改前`);
console.log(Object.keys(product));
console.log('修改后');
Object.defineProperty(product, 'name', {
    enumerable: false
});
console.log(Object.keys(product));
console.log(product.name);

image-20230801194236931

将对象属性描述符的enumerable修改为false后,该属性将不能被迭代,但该属性实际还存在。

修改configurable

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

Object.defineProperty(product, 'name', {
    writable: false,
    configurable: true,
});

Object.defineProperty(product, 'name', {
    value: 'Samsung S21',
});
console.log(product.name);

image-20230801195216203

可以看到我们先将name属性的可重写属性设置为了false,但是我们仍然可以通过属性描述符来改变name的值,因此为了彻底保证name属性不可被重写,我们还需要将configurable属性设置为false

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        this.data = product;
        this.choose = 0;
    }
}

Object.defineProperty(product, 'name', {
    writable: false,
    configurable: false,
});

Object.defineProperty(product, 'name', {
    value: 'Samsung S21',
});
console.log(product.name);

image-20230801195558194

此时再通过属性描述符去修改属性的值时将抛出错误,因为属性描述符定义之后已不可在被修改。

妙用–定义不可被修改的类属性成员

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        Object.defineProperty(this, 'data', {
            value: product,
            writable: false,
            configurable: false,
        })
        this.choose = 0;
    }
}

const uiProduct = new UIProduct(product);
uiProduct.data= 1; //赋值操作不生效
console.log(uiProduct.data);

image-20230801200029428

上述代码我们将UIProduct类的data属性设置为不可被重写的状态,因此后续再去修改该属性时,操作是不生效的。(应用场景,避免用户修改本不应修改的属性)

进阶

属性描述符除了上述几个属性以外还接受两个函数参数,分别为get()set(),一旦我们在属性描述符中声明了get()set()函数,那么该属性的读取和复制操作将转化为执行get()set()函数,看个例子

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        Object.defineProperty(this, 'data', {
            value: product,
            writable: false,
            configurable: false,
        })
        this.choose = 0;
    }
}

Object.defineProperty(product, 'name', {
    get: ()=>{
        console.log('读取操作')
        return 'Xiaomi 11Pro';
    },
    set: (value)=>{
        console.log('写入操作')
    }
});

console.log(product.name);
product.name = 'Xiaomi 11';
console.log(product.name);


image-20230801201418287

可以看到console.log(product.name)其实执行了get(),因为get()返回固定值,因此我们的赋值仅能执行set()函数,而不能修改属性的值。知道属性描述符的这两个函数的作用后,我们可以对妙用–定义不可被修改的类属性成员处的代码做出更好的设计

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        this.choose = 0;
    }
}

const uiProduct = new UIProduct(product);
console.log(uiProduct.data);
uiProduct.data = 'abc';



image-20230801202454637

可以看到我们通过get()set()方法将name属性修改为只读属性,当给该属性赋值时会抛出我们自定的错误,我们还可已利用这一特点进行赋值类型检测

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        let innerChoose = 0;
        Object.defineProperty(this, 'choose', {
            configurable: false,
            get:()=>innerChoose,
            set:(value)=>{
                if (typeof value !== 'number'){
                    throw new Error('choose属性只能赋值为数字');
                }
                if(value<0){
                    throw new Error('choose属性不能为负数');
                }
                innerChoose = value;
            }
        })
    }
}

const uiProduct = new UIProduct(product);
console.log(uiProduct.choose);//执行get()
uiProduct.choose = 2;//执行set()
console.log(uiProduct.choose);
uiProduct.choose = 'abc';//执行set(),类型检测失败抛出错误



image-20230801203248723

拓展

通过上面的内容我们已经可以让类中定义的属性按照我们设计好的方式运行,但是还有不完美的地方

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        let innerChoose = 0;
        Object.defineProperty(this, 'choose', {
            configurable: false,
            get:()=>innerChoose,
            set:(value)=>{
                if (typeof value !== 'number'){
                    throw new Error('choose属性只能赋值为数字');
                }
                if(value<0){
                    throw new Error('choose属性不能为负数');
                }
                innerChoose = value;
            }
        })
    }
}

const uiProduct = new UIProduct(product);
console.log(uiProduct.data);//uiProduct.data不能修改
uiProduct.data.name = 'hahaha,还是能被修改';//但是uiProduct.data对象里的属性可以修改
console.log(uiProduct.data);



image-20230801203800438

上述代码可以发现,如果属性描述符描述的对象是一个对象,那么该对象里的属性是不受属性描述符影响的,那么如何解决呢,我们可以使用Object上的freeze()方法(对象冻结),该方法接受一个对象,然后冻结传入对象,冻结对象以后不能再添加、删除、修改对象的属性

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        product = {...product}//浅拷贝传入参数,避免传入对象被冻结
        Object.freeze(product);//冻结拷贝的对象的属性
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        let innerChoose = 0;
        Object.defineProperty(this, 'choose', {
            configurable: false,
            get:()=>innerChoose,
            set:(value)=>{
                if (typeof value !== 'number'){
                    throw new Error('choose属性只能赋值为数字');
                }
                if(value<0){
                    throw new Error('choose属性不能为负数');
                }
                innerChoose = value;
            }
        })
    }
}

const uiProduct = new UIProduct(product);
console.log(uiProduct.data);
uiProduct.data.name = 'hahaha,还是能被修改';
console.log(uiProduct.data);



image-20230801205140787

可以看到通过冻结操作,我们既避免了data属性内部的属性不可被更改,也没有破坏原始product数据,最后还有一个小问题

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        product = {...product}
        Object.freeze(product);
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        let innerChoose = 0;
        Object.defineProperty(this, 'choose', {
            configurable: false,
            get:()=>innerChoose,
            set:(value)=>{
                if (typeof value !== 'number'){
                    throw new Error('choose属性只能赋值为数字');
                }
                if(value<0){
                    throw new Error('choose属性不能为负数');
                }
                innerChoose = value;
            }
        })
    }
}

const uiProduct = new UIProduct(product);
uiProduct.xxx = 123;//添加任意属性
console.log(uiProduct.xxx);

image-20230801205429869

可以看到UIProduct可以被添加任意属性,这与我们预期不符,如何解决呢

const product = {
    name: 'Xiaomi 11Pro',
    price: 1700,
    brand: 'Xiaomi',
    color: 'white'
}

class UIProduct{
    constructor(product) {
        product = {...product}
        Object.freeze(product);
        Object.defineProperty(this, 'data', {
            configurable: false,
            get:()=>product,
            set:(value)=>{
                throw new Error('Cannot set value for data');
            }
        })
        let innerChoose = 0;
        Object.defineProperty(this, 'choose', {
            configurable: false,
            get:()=>innerChoose,
            set:(value)=>{
                if (typeof value !== 'number'){
                    throw new Error('choose属性只能赋值为数字');
                }
                if(value<0){
                    throw new Error('choose属性不能为负数');
                }
                innerChoose = value;
            }
        })
        Object.seal(this);//将对象本身密封
    }
}

const uiProduct = new UIProduct(product);
uiProduct.xxx = 123;
console.log(uiProduct.xxx);

image-20230801210502527

Object.seal() 方法可以密封一个对象,使其进入一种防篡改的状态。与 Object.freeze() 不同,Object.seal() 只是防止新的属性被加到对象上,并不会阻止对已有属性的修改。Object.seal() 做了以下几件事情:

  1. 将对象设置为不可扩展(non-extensible)。也就是不能再使用 Object.defineProperty() 添加新的属性了。
  2. 将所有现有属性的 configurable 特性设置为 false。这样就不能删除属性或者修改属性的特性了。
  3. 对访问器属性(getter和setter)不产生影响。
  4. 不影响数据属性的值。那些值为writable的属性,值仍然可以修改。
  5. 密封对象上的属性如果原来是可写的,那么仍然可以修改这些属性的值。

总结一下,使用 Object.seal() 后的对象:- 不能添加新属性

  1. - 不能删除已有属性
  2. - 可以修改可写的已有属性的值
  3. - 不能修改属性的可枚举,可配置,可写特性与冻结对象不同,密封对象还是可以修改值的。这提供了一定的防篡改,但不会使对象完全不可变。如果需要一个不可变对象,仍然需要使用 Object.freeze() 来冻结。Object.seal() 可以用于禁止其他代码给对象扩展功能,但仍然允许修改已有数据。

最后,Object.seal()不能阻止我们通过使用对象原型来为对象添加属性,因此如果确实有需要阻止使用对象原型来添加使用我们可以对对象进一步密封 Object.seal(this.prototype)