defineProperty
是vue实现数据劫持的核心,本文一点点的说明defineProperty
怎么实现属性劫持的。
其实我们一般的操作对象属性的方式,增加或者修改属性,均可以使用Object.defineProperty
。
let obj = {};
// 寻常操作:增加/修改 新属性
obj.a = 1;
// 等同于:
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true
});
当然寻常的例子,我们是不会这么玩的,太啰嗦了。
但defineProperty
可以更精确地添加或修改对象的属性。
描述符
先说个专有名词:描述符
。
其实就是defineProperty
的第三个参数,是个对象。这个对象的有以下属性:
- configurable 属性:能不能修改描述符,就是能不能再次修改描述符的其他属性
- enumerable 属性:能不能枚举该属性,就是 a 属性能不能被 for 到
- writable 属性:能不能修改属性值,就是能不能这样修改
obj.a = 1
- value 属性:该属性的值
- get 属性:是个函数,当访问该属性的时候,函数自动调用,函数返回值就是该属性的值
- set 属性:是个函数,当修改该属性的时候,函数自动调用,函数有且只有一个参数,赋值的新值
注意!!!
- 描述符里的
value属性 writable属性
与get属性 set属性
是互斥的关系,只能存在一个 - 另外的属性默认值都是
false
,不想false
的话,记得配置哈,不细说(主要我也不怎么用)。
细说get 和 set
- get 属性:是个函数,当访问该属性的时候,函数自动调用,函数返回值就是该属性的值
- set 属性:是个函数,当修改该属性的时候,函数自动调用,函数有且只有一个参数,赋值的新值
默念三遍,背诵。
写个get 和 set 的例子辅助理解。
这个例子必须掌握,弄懂之后基本就掌握了数据劫持的精髓了
let obj = {};
let value = 1;
Object.defineProperty(obj, "b", {
get() {
console.log("读取b属性", value);
return value;
},
set(newValue) {
console.log("设置b属性", newValue);
value = newValue;
}
});
// 触发get函数,get的返回值就是属性值
// 1
console.log(obj.b);
// 触发set函数,value的值变成了2,注意!!!,此时内存里,属性值并没有改变
obj.b = 2;
// 但是,想要读取属性值的时候,就必然会触发get函数,属性值也自然就改变了,这个思想真的很赞
console.log(obj.b);
这里有个坑:
get
里是不能有读取的操作,不然一直死循环,所以使用到get set
的地方,总需要借助一个变量
所以,这里,变量value
的值就是属性的值,如果想要修改属性,修改 value 的值即可。
这个例子弄懂了,get,set 的精髓,我觉得也就差不多了。
劫持对象的某个属性
有了刚刚例子的基础,试着写写劫持对象的任意一个属性。
function observeKey(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log("读取属性", value);
return value;
},
set(newValue) {
console.log("设置属性", newValue);
value = newValue;
}
});
}
let obj = { a: 1 };
observeKey(obj, "a");
// 读取a,触发get函数
console.log(obj.a);
// 设置a,触发set函数
obj.a = 1;
劫持对象的所有属性
再试试劫持对象的所有属性
其实就是遍历:
function observeObj(obj) {
for (let key in obj) {
// 直接使用 obj.hasOwnProperty会提示不规范
if (Object.prototype.hasOwnProperty.call(obj, key)) {
observeKey(obj, key);
}
}
return obj;
}
function observeKey(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log("读取属性", value);
return value;
},
set(newValue) {
console.log("设置属性", newValue);
value = newValue;
}
});
}
let obj = { a: 1, b: 2 };
observeObj(obj);
console.log(obj);
// 读取a,触发get函数
console.log(obj.a);
// 设置a,触发set函数
obj.a = 1;
劫持对象的所有属性 - 包括对象类型的属性值
上面的有个缺陷,就是当属性值也是对象的时候,不能劫持属性值,如{a:1,c:{b:1}}
简单,递归,补上就行。
function observeObj(obj) {
// 加上参数限制,必须是对象才有劫持,也是递归的终止条件
if (typeof obj !== "object" || obj == null) {
return;
}
for (let key in obj) {
// 直接使用 obj.hasOwnProperty会提示不规范
if (Object.prototype.hasOwnProperty.call(obj, key)) {
observeKey(obj, key);
// 这里劫持该属性的属性值,如果不是对象直接返回,不影响
observeObj(obj[key]);
}
}
return obj;
}
function observeKey(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log("读取属性", value);
return value;
},
set(newValue) {
console.log("设置属性", newValue);
value = newValue;
}
});
}
let obj = { a: 1, b: 2, c: { name: "c" } };
observeObj(obj);
console.log(obj);
// 读取a,触发get函数
console.log(obj.a);
// 设置a,触发set函数
obj.a = 1;
// 触发set函数
obj.c.name = "d";
注意,observeObj
这个函数,不能劫持对象的新增属性,只能劫持对象已有的属性。
defineProperty的缺陷
- 不能监测对象增加属性
- 不能监测对象删除属性
- 不能劫持数组的修改
当然数组的修改可以通过别的方式监测到的,其是通过劫持改变数组方法实现的。
以上缺陷,也是vue里面为啥有$set/$delete
以及对数组只能使用特定方法才能检测到。
let obj = { a: 1, b: [1, 2] };
observeObj(obj);
// 新增属性
obj.c = 3;
// 不会触发get函数
console.log(obj.c);
// 不会触发set函数
obj.b.push(3);
defineProperty还可以挂载属性
其实就是访问options.data.name
可以简写成 options.name
,专业话术,将data上的属性挂载到options上
相当于,用defineProperty
,在options上增加新属性:
// 先挂载单个属性
// options.data相当于source options相当于target
function proxyKey(target, source, key) {
Object.defineProperty(target, key, {
// 这里的source[key]相当于变量value,所以说最简单的那个例子是核心
get() {
return source[key];
},
set(newValue) {
if (newValue === source[key]) {
return;
}
source[key] = newValue;
}
});
}
// 遍历属性,挂载下
function proxyObj(target, source) {
for (let key in source) {
// 直接使用 obj.hasOwnProperty会提示不规范
if (Object.prototype.hasOwnProperty.call(source, key)) {
proxyKey(target, source, key);
}
}
}
let options = {
data: { name: 1 }
};
proxyObj(options, options.data);
// 1
console.log(options.name);
话说,vue的属性劫持和挂载属性,核心原理差不多就是上面这些。
defineProperty还能写日志
比如 obj 有个属性,此属性值经常变化,想要记录其所有变化的值,以此可以形成日志。
let obj = { a: 1 };
let log = [obj.a];
let value = obj.a;
Object.defineProperty(obj, "a", {
get() {
return value;
},
set(newValue) {
if (newValue === value) {
return;
}
value = newValue;
log.push(newValue);
}
});
obj.a = 2;
obj.a = 3;
obj.a = 4;
// [1,2,3,4]
console.log(log);
通用的可以抽离出一个类,专门记录某个值的变化
class Archiver {
constructor() {
let value = null;
this.archive = [];
Object.defineProperty(this, "a", {
get() {
return value;
},
set(newValue) {
if (newValue === value) {
return;
}
value = newValue;
this.archive.push(newValue);
}
});
}
}
let archiver = new Archiver();
archiver.a = 1;
archiver.a = 2;
// [1,2]
console.log(archiver.archive);