Object.defineProperty
文章目录
- 看了几篇网上相关博客,发现存在很多问题,有很多误导性,特此梳理总结一下
理解数据劫持
- 数据劫持,其实就是数据代理。
- 数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
了解Object.defineProperty() 方法
- Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
- obj 要定义属性的对象。
- prop 要定义或修改的属性的名称或 Symbol 。
- descriptor 要定义或修改的属性描述符。
- 属性描述符有两种主要形式:数据描述符和存取描述符。
- 数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。有
configurable
、enumerable
、value
、writable
四个配置属性。 - 存取描述符是由 getter 函数和 setter 函数所描述的属性。有
configurable
、enumerable
、get
、set
四个配置属性。 - 一个描述符只能是这两者其中之一;不能同时是两者。
- 数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。有
当使用了存取描述符get或set方法,不允许使用writable和value这两个属性,同理当使用了数据描述符writable和value,不允许使用get或set方法。
添加对象属性
通过数据描述符创建
const person = {};
Object.defineProperty(person , 'name', {
value: '小陈'
})
console.log(person.name) // 小陈
通过存取描述符修改
const person = {};
Object.defineProperty(person ,'name',{
get:function(){
console.log('触发get');
return '小陈'
},
})
console.log(person.name) //触发get 小陈
修改对象属性值
通过数据描述符修改
writable
- 通过配置writable为true可修改属性的值
const person = {
name: '小陈'
};
Object.defineProperty(person, 'name', {
writable: true,
})
console.log(person.name) // 小陈
person.name = '小王';
console.log(person.name) //小王
当不配置writable时,其默认值是false,属性的值(value)是不能被赋值运算符改变;
当配置writable为true时,属性的值(value)才能被运算符改变;
所以再给name属性值重新赋值,并不会改变name的值。
通过存取描述符修改
const person = {
name: '小陈'
};
let nameValue = person.name
Object.defineProperty(person, 'name', {
configurable: true,
get: function() {
console.log('触发get');
return nameValue
},
set: function(newValue) {
console.log('触发set')
// person['name'] = newValue 错误写法,会导致栈溢出
nameValue = newValue
}
})
console.log(person.name) //触发get 小陈
person.name = '小王'; //触发set
console.log(person.name) //触发get 小王
配置对象属性
- configurable 、enumerable配置对数据描述符和存取描述符设置效果一致。
configurable
- 表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。
- configurable为默认值或者配置为false时,则该属性相关的配置不能再被更改,也不能被删除
const person = {};
Object.defineProperty(person, 'name', {
value: '小陈',
configurable: false,
})
console.log(person.name);
Object.defineProperty(person, 'name', {
writable: true, // Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
configurable: true, // Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
enumerable: true, // Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
value: '小王' // Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
get() {
return '小王' // Cannot redefine property: name
}
})
Object.defineProperty(person, 'name', {
set() {} //Cannot redefine property: name
})
delete person.name // 由于前边抛错,不会执行到这里,即使执行到这里也不会删除该属性
console.log(person);
- 以上将configurable设为true时,则不会抛出任何错误,最后该属性会被删除。
注意:configurable: true时,可以通过Object.defineProperty给同一属性重复定义配置。同时数据描述符配置属性writable为false,则不能通过赋值方式改变属性的值,但是可以通过Object.defineProperty重复定义的方法改变。
const person = {};
Object.defineProperty(person, 'name', {
value: '小陈',
configurable: true,
writable: false,
})
console.log(person.name); // 小陈
person.name = '小王';
console.log(person.name); // 小陈
Object.defineProperty(person, 'name', {
value: '小王',
})
console.log(person.name); // 小王
enumerable
- enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。默认值为 false。
const person = {};
Object.defineProperty(person, "name", {
value: '小陈',
enumerable: true
});
Object.defineProperty(person, "age", {
value: '20',
enumerable: false
});
Object.defineProperty(person, "sex", {
value: '男',
enumerable: true
});
Object.defineProperty(person, "hobby", {
value: '睡觉',
enumerable: true
});
for (var i in person) {
console.log(i); // name sex hobby
}
const result = Object.keys(person);
console.log(result); // ['name', 'sex', 'hobby']
- 通过propertyIsEnumerable方法确定属性是否可枚举
console.log(person.propertyIsEnumerable('age')); // false
数据描述符中的属性默认值问题
- 使用点运算符和 Object.defineProperty() 为对象的属性赋值时,描述符中的属性默认值是不同的
var o = {};
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,
writable: false,
configurable: false,
enumerable: false
});
- 即通过点运算符创建的对象属性是可修改可删除可枚举, writable、configurable、enumerable都为true
自定义一个getters和setters
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
Object.defineProperty的应用
监听对象上的多个属性
错误写法
const person = {
name:'小陈',
age:20,
sex:'男',
}
Object.keys(person).forEach(function (key) {
Object.defineProperty(person, key, {
enumerable: true,
configurable: true,
get() {
return person[key]
},
set(val) {
console.log(`对person中的${key}属性进行了修改`)
person[key] = val
}
})
})
console.log(person.age) // Uncaught RangeError: Maximum call stack size exceeded
这种写法在get/set里写person[key],进入递归调用状态,反复访问属性值。最终造成栈溢出。
正确写法
const person = {
name: '小陈',
age: 20,
sex:'男'
}
// 实现一个响应式函数
function defineProperty(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`)
return val
},
set(newVal) {
console.log(`${key}属性被修改为${newVal}了`)
val = newVal
}
})
}
// 实现一个遍历函数Observer
function observer(obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
}
Observer(person);
console.log(person.age); // 访问了age属性 20
person.age = 24; // age属性被修改为24了
console.log(person.age); // 访问了age属性 24
深度监听一个对象
- 借助Object.defineProperty与递归的思想可达到深度监听的效果
function defineProperty(obj, key, val) {
//如果某对象的属性也是一个对象,递归进入该对象,进行监听
if (typeof val === "object") {
observer(val);
}
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`);
return val;
},
set(newVal) {
// 如果newVal是一个对象,递归进入该对象进行监听
if (typeof val === 'object') {
observer(key)
}
console.log(`${key}属性被修改为${newVal}了`);
val = newVal;
},
});
}
function observer(obj) {
//如果传入的不是一个对象,return
if (typeof obj !== "object" || obj === null) {
return;
}
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key]);
});
}
Object.defineProperty的不足
不支持监听数组的变化
let arr = [1, 2, 3]
let obj = {}
//把arr作为obj的属性监听
Object.defineProperty(obj, 'arr', {
get() {
console.log('触发了get')
return arr
},
set(newVal) {
console.log('触发了set', newVal)
arr = newVal
}
})
console.log(obj.arr) //触发了get [1,2,3]
obj.arr = [1, 2, 3, 4] //输出set [1,2,3,4] 正
obj.arr.push(5) //触发了get
obj.arr.unshift() //触发了get
obj.arr.pop() // //触发了get
obj.arr.shift() 触发了get
当使用Object.defineProperty监听的对象属性是数组时,使用push、unshift、pop、shift、splice, ‘sort’, reverse是监听是触发不了set的。
只要不是重新赋值一个新的数组对象,任何对数组内部的修改都不会触发set方法的执行。
- 在Vue2.x中,通过重写Array原型上的方法解决了这个问题的。
通过重写数组操作方法实现数组监听
function arrMethods() {
const orginalProto = Array.prototype;
const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(method => {
arrayProto[method] = function() {
// 执行原始操作
orginalProto[method].apply(this, arguments)
console.log('监听成功', method)
}
})
return arrayProto
}
function observer(data) {
if (Array.isArray(data)) {
// 如果是数组, 重写原型
data.__proto__ = arrMethods()
// 传入的数据可能是多维度的,也需要执行响应式
for (let i = 0; i < data.length; i++) {
observer(data[i])
}
}
}
const arr = [1, 2, 3, 4]
observer(arr);
arr.push(5); //监听成功
实现监听对象数组
//重写数组操作方法
function arrMethods() {
const orginalProto = Array.prototype;
const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(method => {
arrayProto[method] = function() {
// 执行原始操作
orginalProto[method].apply(this, arguments)
console.log('监听成功', method)
}
})
return arrayProto
}
// 劫持
function defineProperty(obj, key, val) {
//如果某对象的属性也是一个对象,递归进入该对象,进行监听
if (typeof val === "object") {
observer(val);
}
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`);
return val;
},
set(newVal) {
// 如果newVal是一个对象,递归进入该对象进行监听
if (typeof val === 'object') {
observer(key)
}
console.log(`${key}属性被修改为${newVal}了`);
val = newVal;
},
});
}
// 多属性监听
function observer(data) {
//如果传入的不是一个对象,return
if (typeof data !== "object" || data === null) {
return;
}
if (Array.isArray(data)) {
// 如果是数组, 重写原型
data.__proto__ = arrMethods()
// 传入的数据可能是多维度的,也需要执行响应式
for (let i = 0; i < data.length; i++) {
observer(data[i])
}
} else {
// 如果是对象
Object.keys(data).forEach((key) => {
defineProperty(data, key, data[key]);
});
}
}
// 数组
const arr = [1, 2, 3, 4]
observer(arr)
arr.push(5) // 监听成功 push
// 对象
const person = {
name: '小陈',
age: 20,
sex: '男',
arr: [1, 2, 3, 4]
}
observer(person)
console.log(person.age); // 访问了age属性 20
person.age = 24; // age属性被修改为24了
console.log(person.age); // 访问了age属性 24
person.arr.push(5) // 监听成功 push
不能监听属性新增和删除操作
const person = {
name: '小陈'
};
let nameValue = person.name
Object.defineProperty(person, 'name', {
configurable: true,
get: function() {
console.log('触发get');
return nameValue
},
set: function(newValue) {
console.log('触发set')
nameValue = newValue
}
})
person.age = 20; // 并没有触发set
console.log(person);
delete person.age; //并没有触发set
console.log(person);
什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?
- 每次访问 a 返回的值都不一样,那么肯定会想到数据劫持。
let current = 0
Object.defineProperty(window, 'a', {
get() {
current++
return current
}
})
// console.log(a); 1
// console.log(a); 2
// console.log(a); 3
console.log(a === 1 && a === 2 && a === 3) // true