给对象定义可拦截属性
defineProperty
从名字也能猜出个大概来,「定义属性」,没错,它就是用来给对象定义属性的,只不过它可以在定义属性的时候对其进行拦截操作:
let json = { _a: 10 };
Object.defineProperty(json, "a", {
get() {
console.log("get被调用了");
return json._a;
},
set(val) {
console.log("set被调用了");
json._a = val;
},
});
我们使用defineProperty
来给json
加了一个a属性,这个时候我们一量获取、设置a
,就会分别触发里面的get
和set
。在这两个方法里面,我们就可以对其实进行拦截,并且对属性值做一些处理:
上面我们在set
和get
里只是做了一个简单的「获取」和「设置」操作,其实我们可以对数据做一些其它复杂的计算之后再进行操作。当然,这是根据我们实际的需求来确定的。
删除属性
我们来把刚才添加的a
属性给删掉试一下:
删除a
属性的时候返回了false
,删除失败,这也是defineProperty
添加属性的一个默认效果,那么如果想要能删除属性该如何操作呢?
configurable
其实在使用defineProperty
的时候还有一个configurable
属性,好多属性通过名字就知道它是干啥的-「是否可配制」,这个属性默认就是false
,也就是不可配制,所以我们在删除自定义属性的时候就回返回false
。接下来我们把它设置成true
来试一下:
简单应用
在前面的例子当中,我们是给一个json赋值,现在我们来看这个例子:
let app = new App({
root: "#app",
data() {
return {
username: "vvheat",
age: 18,
};
},
});
上面这段代码用过vue
的朋友应该很熟悉吧?把App
改成Vue
不就是新建Vue实例了吗?当然,我们这点代码肯定比vue差了几个宇宙了。上面我们新建了一个App
的实例叫app
,我们想在设置它里面data的值的时候通过app.username
、app.age
这样来操作,但现在肯定是不可以的,因为username
和age
是data
方法返回的一个新对象,并不是直接给app
加的属性,那么如何才能实现呢?
我们直接来看App
这个类:
class App {
constructor(options) {
let data = options.data();
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
console.log(`有人获取了${key}`);
return data[key];
},
set(val) {
console.log(`有人设置了${key}`);
data[key] = val;
},
});
}
}
}
我们把实例当中data里的属性全部通过defineProperty
放到了App
的实例上,这样,我们就可以直接在实例上找到所有在data()
里的属性了:
WOW!!Amazing!!!
其实defineProperty
并不是万能的,比如我们来给app
实例添加一个hobbies
的数组:
let app = new App({
root: "#app",
data() {
return {
username: "vvheat",
age: 18,
hobbies: ["B-ball", "Painting"],
};
},
});
然后我们来设置一下hobbiles里的内容:
好像没啥问题啊,诶?是不是少打了什么东西?之前每「设置」一个属性的时候都会出现 有人设置了***
,可这次为什么没有了呢?其实这是defineProperty
的一个缺点,就是它不会拦截「数组元素」和「对象属性」!!!它只能拦截第一层属性的变化!!!
可以看到,我们直接设置app.hobbiles
的时候,才会出现有人设置了hobbies
。那么我们如何来处理这种情况呢?
我们在实际场景中,设置属性的时候肯定不只是打印一下这么简单,一般我们在设置属性的时候会执行一些渲染的操作:
class App {
constructor(options) {
let data = options.data();
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return data[key];
},
set(val) {
data[key] = val;
this.render();
},
});
}
}
render() {
console.log("执行渲染操作!");
}
}
上面的render
只是打印了一句话,但实际上我们会在这里做一些页面的渲染工作,这里不是我们要说的重点,所以略过。
我们可以看出来,只要我们设置了一个属性,就会调用render
方法,但是我们如果要设置一个数组元素或者对象属性,它不会被拦截。所以我们需要单独写一个方法来供我们进行调用:
class App {
constructor(options) {
let data = options.data();
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return data[key];
},
set(val) {
data[key] = val;
this.render();
},
});
}
}
$set(obj, key, val) {
obj[key] = val;
this.render();
}
render() {
console.log("执行渲染操作!");
}
}
我们为之前的类添加了一个$set
方法,这个方法很简单,只是把传进来的东西做一个赋值,然后渲染,所以,当我们给数组元素或者对象设置值的时候就可以这样来用:
这样,我们就单独针对数组元素和对象属性进行了处理。但现在又会导致一个新的问题,这个$set
方法也可以用在根层的属性上:
可以看到,我直接通过app.username
来设置的时候,渲染一次是正确的,但是我通过app.$set
设置的时候,却渲染了两次!一次是$set
里面调用我们很清楚,还有一次是defineProperty
拦截掉了,这样很明显是不正确的。所以我们要处理一下:
class App {
constructor(options) {
let data = options.data();
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return data[key];
},
set(val) {
data[key] = val;
this.render();
},
});
}
this._updated = false;
}
$set(obj, key, val) {
this._updated = false;
obj[key] = val;
if (!this._updated) {
this.render();
}
}
render() {
console.log("执行渲染操作!");
this._updated = true;
}
}
上面我们添加了一个标识_updated
,在$set
当中,我们判断了是否已经执行过render
,如果没有才进行渲染,如果执行过,那么肯定是在defineProperty
当中调用的。这样就是我们想要的结果。
到这里我们已经知道了defineProperty
对于数组、json的监听拦截是不完整的,如果想要多层的监听,必须得用递归,但一般不会这样做,因为性能会比较低。