跳到主要内容

defineProperty

2023年06月08日
柏拉文
越努力,越幸运

一、认识


Object.defineProperty() 方法会精确地添加或修改对象的属性,并返回此对象。

二、语法


Object.defineProperty(obj, prop, descriptor)
  • obj: 要定义属性的对象。

  • prop: 要定义或修改的属性的名称或 Symbol 。

  • descriptor: 要定义或修改的属性描述符。对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符

2.1 数据描述符

数据描述符: 数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。一个描述符只能是数据描述符和存取描述符两者其中之一;不能同时是两者。如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

  • configurable: 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。

    • 默认值: false
  • enumerable: 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

    • 默认值: false
  • value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。

    • 默认值: undefined
  • writable: 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。writable 属性设置为 false 时,该属性被称为不可写的。它不能被重新赋值。

    • 默认值: false

2.2 存取描述符

存取描述符: 存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是数据描述符和存取描述符两者其中之一;不能同时是两者。如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

  • configurable:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。

    • 默认值: false
  • enumerable: 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

    • 默认值: false
  • get: 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。

    • 默认值: undefined
  • set: 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

    • 默认值: undefined

三、返回值


被传递给函数的对象。

四、灵活用法


4.1 监听对象属性

const obj = {
name:'柏拉图',
age:23
};

Object.keys(obj).forEach(key=>{
let value = obj[key];
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
console.log(`${key}获取值`);
return value;
},
set(newValue,oldValue){
console.log(`${key}设置值`);
value = newValue;
}
});
});

console.log(obj.name);
obj.name = '柏拉图修改';
console.log(obj.name);

4.2 监听数组元素

const array = [10, 20, 30, 40, 50];

array.forEach((value, index, arr) => {
let valueCopy = value;
Object.defineProperty(array, index, {
enumerable: true,
configurable: true,
get() {
console.log(`${index}获取值`);
return valueCopy;
},
set(newValue, oldValue) {
console.log(`${index}设置值`);
valueCopy = newValue;
},
});
});

console.log(array[2]);
array[2] = "30修改";
console.log(array[2]);

4.3 对象属性默认值

  • o.a = 1
o.a = 1;
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true
});
  • Object.defineProperty(o, "a", { value : 1 });
// 另一方面,
Object.defineProperty(o, "a", { value : 1 });
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: false,
configurable: false,
enumerable: false
});

4.4 象属性增、删、改、查

  • 场景 2.1、通过数据描述符创建、修改、删除属性

    // 数据描述符
    const obj = {};
    Object.defineProperty(obj,'name',{
    value:'柏拉图',
    writable:true, // writable 为 true :该属性可被赋值运算符改变
    enumerable:true, // enumerable 为 true : 该属性可被枚举
    configurable:true // configurable 为 true : 该属性可被删除
    });

    console.log(obj.name);
    obj.name = '柏拉图修改';
    console.log(obj.name);

    for(let value in obj){
    console.log(value);
    }

    delete obj.name;
    console.log(obj);
  • 场景 2.2、通过存取描述符创建、修改、删除属性-方案一通过第三方变量

    // 存取描述符
    const obj = {};
    let objValue = '柏拉图';
    Object.defineProperty(obj,'name',{
    enumerable:true, // enumerable 为 true : 该属性可枚举
    configurable:true, // configurable 为 true : 该属性可删除
    get(){
    console.log('访问操作'); // 每次访问 obj.name ,调用 get() 方法
    return objValue;
    },
    set(newValue){
    console.log('设置操作'); // 每次修改 obj.name , 调用 set() 方法
    objValue = newValue;
    }
    });

    console.log(obj.name);
    obj.name = '柏拉图修改';
    console.log(obj.name);
    for(let key in obj){
    console.log(key);
    }
    delete obj.name;
    console.log(obj.name);
  • 场景 2.3、通过存取描述符创建、修改、删除属性-方案一通过this

    const obj = {};
    Object.defineProperty(obj,'name',{
    enumerable:true, // enumerable 为 true : 该属性可枚举
    configurable:true, // configurable 为 true : 该属性可删除
    get(){
    console.log('访问操作'); // 每次访问 obj.name ,调用 get() 方法
    return this._name;
    },
    set(newValue){
    console.log('设置操作'); // 每次修改 obj.name , 调用 set() 方法
    this._name = newValue;
    }
    });

    console.log(obj.name);
    obj.name = '柏拉图修改';
    console.log(obj.name);
    for(let key in obj){
    console.log(key);
    }
    delete obj.name;
    console.log(obj.name);

4.5 数组元素增、删、改、查

const array = [];
Object.defineProperty(array,'0',{
value:'哈哈',
writable:true,
enumerable:true,
configurable:true
});
Object.defineProperty(array,'1',{
enumerable:true,
configurable:true,
get(){
console.log('访问 1');
return this._one || '哈哈';
},
set(newValue,oldValue){
console.log('设置 1');
this._one = newValue;
}
});
console.log(array);
console.log(array[1]);
array[1] = '哈哈修改';
console.log(array[1]);

五、应用场景


5.1 计算属性

let quantity = 2;
const product = {
price: 10,
quantity: quantity
};

function computed() {
return product.price * product.quantity;
}

Object.defineProperty(product, 'quantity', {
get() {
return quantity;
},
set(value) {
quantity = value;
computed();
}
});

product.quantity = 2;
console.log(`总价为: ${computed()}`);
product.quantity = 10;
console.log(`总价为: ${computed()}`);

5.2 获取对象

需求: 有一个对象 obj , 我们需要不改变现有代码的情况下, 修改 obj 对象, 问题如下:

const o = (function () {
const obj = {
a: 1,
b: 2
};
return {
get(key) {
return obj[key];
}
};
})();


// 不可以修改上述代码, 来修改 obj 对象

思路: 我们不能改变已有代码,因此我们必须要通过一种方式获取 obj 对象自身, 所以,我们可以通过 Object.defineProperty 来获取 obj 对象

const obj = {
a: 1,
b: 2
};

Object.defineProperty(Object.prototype, 'self', {
get() {
console.log('this', this); // this { a: 1, b: 2}
return this;
}
});

const objSelf = obj.self;
console.log(objSelf === obj); // true

如图所示, 我们通过 Object.defineProperty 来设置并监听 Object.prototype 中的 self 属性, 当有对象访问 .self 的时候,get() 中的 this 就是当前对象

解决

const o = (function () {
const obj = {
a: 1,
b: 2
};
return {
get(key) {
return obj[key];
}
};
})();

console.log(o.get('a'));

Object.defineProperty(Object.prototype, 'self', {
get() {
return this;
}
});

const obj = o.get('self');
console.log(obj);
obj.c = 3;
console.log(o.get('c'));

防止: 那么,如何防止修改对象原型 Objedct.prototype 来获取当前对象呢?

  • 方案一、修改当前对象原型为 null

     const o = (function () {
    const obj = {
    a: 1,
    b: 2
    };
    Object.setPrototypeOf(obj, null);
    return {
    get(key) {
    return obj[key];
    }
    };
    })();

    console.log(o.get('a'));

    Object.defineProperty(Object.prototype, 'self', {
    get() {
    return this;
    }
    });

    const obj = o.get('self');
    console.log(obj);
    obj.c = 3;
    console.log(o.get('c'));
  • 方案二、检测 key 是否为当前对象自身属性

    const o = (function () {
    const obj = {
    a: 1,
    b: 2
    };
    return {
    get(key) {
    if(obj.hasOwnProperty(key)){
    return obj[key];
    }
    return;
    }
    };
    })();

    console.log(o.get('a'));

    Object.defineProperty(Object.prototype, 'self', {
    get() {
    return this;
    }
    });

    const obj = o.get('self');
    console.log(obj);
    obj.c = 3;
    console.log(o.get('c'));

六、自身缺陷


Object.defineProperty() 不会检测 JavaScript 对象和数组的变化。这是因为: Object.defineProperty() 设计的初衷,就不是为了去监听拦截一个对象中的所有属性的。所以通过Object.defineProperty()实现的监听仅仅是已经定义好的属性或者元素才可以的。后续添加的属性或者元素都不可以监听到。另外,Object.defineProperty() 方法仅仅能够监听获取属性值和设置属性值两种操作而已,其他的删除属性、获取属性描述符、设置属性描述符、获取原型、设置原型、检测对象属性等其他对对象属性的复杂操作都监听不到。

6.1 监听对象缺陷

Object.defineProperty() 能够监听对象已有属性的获取设置,监听不到新增的属性,也监听不到已有属性的删除属性、获取属性描述符、设置属性描述符、获取原型、设置原型、检测对象属性。

通过 Object.defineProperty() 监听后续添加对象属性时
const obj = {
name:'柏拉图',
age:23
};

Object.keys(obj).forEach(key=>{
let value = obj[key];
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
console.log(`${key}获取值`);
return value;
},
set(newValue,oldValue){
console.log(`${key}设置值`);
value = newValue;
}
});
});

obj.newAttr = '新属性';
console.log(obj.newAttr); // 没有触发 get
obj.newAttr = '新属性变化'; // 没有触发 set
console.log(obj.newAttr); // 没有触发 set

6.2 监听数组缺陷

Object.defineProperty() 能够监听数组已有元素的获取设置,监听不到新增的元素。

通过 Object.defineProperty() 监听后续添加数组元素时
const array = [10, 20, 30, 40, 50];

array.forEach((value, index, arr) => {
let valueCopy = value;
Object.defineProperty(array, index, {
enumerable: true,
configurable: true,
get() {
console.log(`${index}获取值`);
return valueCopy;
},
set(newValue, oldValue) {
console.log(`${index}设置值`);
valueCopy = newValue;
}
});
});

console.log(array[3]); // 触发 get
array[3] = 300; // 触发 set
console.log(array[3]); // 触发 get

console.log(array[5]); // 没有触发 get
array[5] = '50修改'; // 没有触发 set
console.log(array[5]); // 没有触发 get

七、原理(Polyfill)