defineProperty
一、认识
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
特性表示对象的属性是否可以被删除,以及除value
和writable
特性外的其他特性是否可以被修改。- 默认值: 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
特性表示对象的属性是否可以被删除,以及除value
和writable
特性外的其他特性是否可以被修改。- 默认值: 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()
能够监听对象已有属性的获取和设置,监听不到新增的属性,也监听不到已有属性的删除属性、获取属性描述符、设置属性描述符、获取原型、设置原型、检测对象属性。
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()
能够监听数组已有元素的获取和设置,监听不到新增的元素。
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