初识defineProperty
Object.defineProperty(),是一个Object对象上的方法,用于创建对象上的属性。
基本语法
Object.defineProperty(object, prop, desp);
其中,object指明要添加属性的对象名,prop指出属性名,而desp则是一些属性描述符。
var phone = {name: 'nava7'};
Object.defineProperty(car, 'price', {
value: 7000,
configurable: false,
writable: true,
enumerable: true,
})
下面对属性描述符做一番简单介绍。
configurable
指定属性是否可以在第一次设置后被修改、是否可以被删除。
比如,如下代码在严格模式下,会报错;在一般模式下,修改、删除无效。
var person = {name: 'Yank'};
Object.defineProperty(person, 'age', {
value: 20,
configurable: false,
writable: true,
enumerable: false,
})
console.log(person);
delete person.age
console.log(person);
//删除无效,结果仍为 {name: 'Yank', age: 20}
enumerable
enumerable配置项用于描述属性是否可以被for in 或者Object.keys()的遍历(枚举)中。
当enumerable为false时,它不可以被上述两个语句遍历到。
var school = {name: 'XiWang Primary School'};
Object.defineProperty(school, 'age', {
value: 55,
configurable: true,
enumerable: false,
})
Object.defineProperty(school, 'address', {
value: 'Hubei Province',
configurable: true,
enumerable: true,
})
// 打印结果,只有address属性,而没有age属性
console.log(Object.keys(school));
// ['name', 'address']
writable
比较好理解,writable为false时,表示该属性是只读的,不可以被写,且在严格模式下,写操作会报错;一般模式下,写操作失效。
get/set
get方法用于获取属性,而set方法用于设置属性。注意,设置了getter和setter之后,不可以再设置value和writable了,否则会报错。
一般地,使用第三方变量避免getter和setter反复无限调用:
// 错误写法
var province = {name: 'Hubei'};
Object.defineProperty(province, 'centerCity', {
get: function() {
console.log('调用了getter');
return province.centerCity
},
set: function(val) {
console.log('调用了setter');
province.centerCity = val
}
})
province.centerCity = 'Wuhan'
// 正确写法,另设了一个变量str
var province = {name: 'Hubei'};
var str = '';
Object.defineProperty(province, 'centerCity', {
get: function() {
console.log('调用了getter');
return str
},
set: function(val) {
console.log('调用了setter');
console.log(val); // Wuhan
str = val
}
})
province.centerCity = 'Wuhan'
使用Object.defineProperty()可以为对象新增属性,并且还可以细化地指定属性描述符,默认三个属性描述符(configurable、writable、enumerable)均为false;
而另一种直接的方式Object.prop即点运算符新增属性,则是默认三个属性描述符均为true。
属性描述符的分类
- 数据描述符:value和writable
- 存取描述符:get和set
注意,两种描述符不可以同时使用。
与Vue中的数据代理
通过一个对象代理对另一个对象中属性的操作(读/写),叫做数据代理。
下面举一个很简单的栗子:
var obj1 = {
x: 1,
}
var obj2 = {
y: 2,
}
Object.defineProperty(obj2, 'x', {
configurable: true,
enumerable: true,
get() {
console.log("getter of obj2 is working!");
return obj1.x;
},
set(val) {
console.log("setter of obj2 is working!");
obj1.x = val;
}
})
在这个栗子中,我们就实现了obj1和obj2之间的数据代理。我们要想读取/修改obj1.x,只需要读取/修改obj2.x就好,因为会触发其getter/setter。
下面进入正题:Vue中的数据代理究竟是什么?
在vue2中,我们在构建vue实例对象时传入各项配置参数:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="root">
<h2>姓名:{{ name }}</h2>
<h2>住址:{{ address }}</h2>
</div>
</body>
<script>
Vue.config.productionTip = false
const vm = new Vue({
el: '#root',
data: {
name: '胡图图',
address: '翻斗花园'
}
})
</script>
</html>
在浏览器中查看执行结果,在控制台输出vm,可以发现name、address都已经变成了vm实例自身的属性了,并且都有自己的getter/setter。
但是数据代理在vue中是如何体现的呢?首先我们需要知道的是,vm在拿到我们配置的属性name、address后,一定会将其保留下来。事实上,它们存在于vm的 _ data属性中了。也就是说,接下来,对 _ data中属性的修改,就相当于是对vm上属性的修改了(数据代理)。
为了能够验证这一点,我们需要想方法拿到原始的配置对象(options)上的data参数,故作出如下修改:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="root">
<h2>姓名:{{ name }}</h2>
<h2>住址:{{ address }}</h2>
</div>
</body>
<script>
Vue.config.productionTip = false
// 小技巧,将data配置项单独全局声明,方便在控制台直接获取
var myData = {
name: '胡图图',
address: '翻斗花园'
}
const vm = new Vue({
el: '#root',
data: myData,
})
console.log(vm);
console.log(myData);
console.log(vm._data === myData); // 验证data配置项就是vm的_data属性,故下文中不对两者(配置项data属性与vm上的_data属性)作刻意区分
</script>
</html>
现在在浏览器控制台作出如下操作:vm.name = 'hututu'
我们就会发现姓名变成了‘hututu’。从数据代理角度来说,我们修改的是vm上的name属性,也就调用了其setter,从而改变了与之关联的data( _ data)上的name。
现在对整个数据代理过程梳理一下:
- 首先,我们书写了如下代码:
const vm = new Vue({ el: '#root', data: { name: '胡图图', address: '翻斗花园' } })
- 在创建好的vm实例对象上,就会有一个准备好的 _ data属性,里面存放了options配置项中的data,大致如下:
// vm { ... _data: { name: '胡图图', address: '翻斗花园', }, ... }
- 到这里,还没有出现数据代理。但是,如果Vue仅仅到此为止的话,模板中的语法就应该这样书写了:
原因很简单,此时vm实例对象上还没有name、address属性,只有 _ data属性,通过它获取到数据。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> </head> <body> <div id="root"> <h2>姓名:{{ _data.name }}</h2> <h2>住址:{{ _data.address }}</h2> </div> </body> <script> Vue.config.productionTip = false var myData = { name: '胡图图', address: '翻斗花园' } const vm = new Vue({ el: '#root', data: myData, }) console.log(vm); console.log(myData); console.log(vm._data === myData); </script> </html>
- 数据代理,也就是再vm身上添加了相应的属性:
而这里两者之间就是通过Object.defineProperty()建立的数据代理关系:// vm { ... _data: { name: '胡图图', address: '翻斗花园', }, ... name: ... address: ... ... }
通过这个方法,将data对象中的所有属性添加到vm上,同时为每一个属性指定一个getter/setter,在getter/setter内部去操作data对象中对应的属性。
可见,数据代理,大大地简化了代码书写,也使得更加方便地操作data中的数据了。
与数据劫持
数据劫持,就是对某一项数据进行访问、设置时,会触发相应的函数,并返回最后想要的结果(或返回属性值getter,或修改属性值setter)。当然,我们就可以在函数中另外做一些我们自己想要的操作了,这就是数据劫持。
在vue2中,就是使用defineProperty()实现的数据劫持,当数据被修改时,就会通知该数据被修改了。通俗地理解,就是通过defineProperty()修改了属性的getter/setter,并且在其中增添了监视功能。