Object.defineProperty() 和 Proxy 对象,均可以用来对数据的劫持操做。何为数据劫持呢?就是在咱们访问或者修改某个对象的某个属性的时候,经过一段代码进行拦截行为,而后进行额外的操做,而后返回结果。那么vue中双向数据绑定就是一个典型的应用。javascript
Vue2.x 是使用 Object.defindProperty(),来进行对对象的监听的。
Vue3.x 版本以后就改用Proxy进行实现的。
下面咱们先来理解下Object.defineProperty做用。css
一: 理解Object.defineProperty的语法和基本做用。html
在理解以前,咱们先来看看一个普通的对象,对象它是由多个名/值对组成的无序集合。对象中每一个属性对于任意类型的值。
好比如今咱们想建立一个简单的对象,能够简单的以下代码:vue
const obj = new Object; // 或 const obj = {}; obj.name = 'kongzhi'; console.log(obj.name); // 在控制台中会打印 kongzhi obj.xxx = function() { console.log(111); } // 调用 xxx 方法 obj.xxx(); // 在控制台中会打印 111
可是除了上面添加对象属性以外,咱们还可使用 Object.defineProperty 来定义新的属性或修改原有的属性。最终会返回该对象。
接下来咱们慢慢来理解下该用法。java
基本语法:git
Object.defineProperty(obj, prop, descriptor);
基本的参数解析以下:github
obj: 能够理解为目标对象。
prop: 目标对象的属性名。
descriptor: 对属性的描述。数组
那么对于第一个参数obj 和 prop参数,咱们很容易理解,好比上面的实列demo,咱们定义的 obj对象就是第一个参数的含义,咱们在obj中定义的name属性和xxx属性是prop的含义,那么第三个参数描述符是什么含义呢?app
descriptor: 属性描述符,它是由两部分组成,分别是:数据描述符和访问器描述符,数据描述符的含义是:它是一个包含属性的值,并说明这个属性值是可读或不可读的对象。访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。框架
下面咱们继续来理解下 数据描述符 和 访问器描述符具体包含哪些配置项含义及用法。
1.1 数据描述符
const obj = { name: 'kongzhi' }; // 对obj对象已有的name属性添加数据描述 Object.defineProperty(obj, 'name', { configurable: true | false, enumerable: true | false, value: '任意类型的值', writable: true | false }); // 对obj对象添加新属性的描述 Object.defineProperty(obj, 'newAttr', { configurable: true | false, enumerable: true | false, value: '任意类型的值', writable: true | false });
如上代码配置,数据描述符有如上configurable,enumerable,value 及 writable 配置项。
下面咱们来看下 每一个描述符中每一个属性的含义:
1)value
属性对应的值,值的类型能够是任意类型的。好比我先定义一个obj对象,里面有一个属性 name 值为 'kongzhi', 如今咱们经过以下代码改变 obj.name 的值,以下代码:
const obj = { name: 'kongzhi' }; // 对obj对象已有的name属性添加数据描述 Object.defineProperty(obj, 'name', { value: '1122' }); console.log(obj.name); // 输出 1122
若是上面我不设置 value描述符值的话,那么它返回的值仍是 kongzhi 的。好比以下代码:
const obj = { name: 'kongzhi' }; // 对obj对象已有的name属性添加数据描述 Object.defineProperty(obj, 'name', { }); console.log(obj.name); // 输出 kongzhi
2)writable
writable的英文的含义是:'可写的',在该配置中它的含义是:属性的值是否能够被重写,设置为true能够被重写,设置为false,是不能被重写的,默认为false。
以下代码:
const obj = {}; Object.defineProperty(obj, 'name', { 'value': 'kongzhi' }); console.log(obj.name); // 输出 kongzhi // 改写obj.name 的值 obj.name = 111; console.log(obj.name); // 仍是打印出 kongzhi
上面代码中 使用 Object.defineProperty 定义 obj.name 的值 value = 'kongzhi', 而后咱们使用 obj.name 进行从新改写值,再打印出 obj.name 能够看到 值 仍是为 kongzhi , 这是 Object.defineProperty 中 writable 默认为false,不能被重写,可是下面咱们将它设置为true,就能够进行重写值了,以下代码:
const obj = {}; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true }); console.log(obj.name); // 输出 kongzhi // 改写obj.name 的值 obj.name = 111; console.log(obj.name); // 设置 writable为true的时候 打印出改写后的值 111
3)enumerable
此属性的含义是:是否能够被枚举,好比使用 for..in 或 Object.keys() 这样的。设置为true能够被枚举,设置为false,不能被枚举,默认为false.
以下代码:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true }); // 枚举obj的属性 for (const i in obj) { console.log(i); // 打印出 name1 }
如上代码,对象obj自己有一个属性 name1, 而后咱们使用 Object.defineProperty 给 obj对象新增 name属性,可是经过for in循环出来后能够看到 只打印出 name1 属性了,那是由于 enumerable 默认为false,它里面的值默认是不可被枚举的。可是若是咱们将它设置为true的话,那么 Object.defineProperty 新增的属性也是能够被枚举的,以下代码:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true }); // 枚举obj的属性 for (const i in obj) { console.log(i); // 打印出 name1 和 name }
4) configurable
该属性英文的含义是:可配置的意思,那么该属性的含义是:是否能够删除目标属性。若是咱们设置它为true的话,是能够被删除。若是设置为false的话,是不能被删除的。它默认值为false。
好比以下代码:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true }); // 使用delete 删除属性 delete obj.name; console.log(obj.name); // 打印出kongzhi
如上代码 使用 delete命令删除 obj.name的话,该属性值是删除不了的,由于 configurable 默认为false,不能被删除的。
可是若是咱们把它设置为true,那么就能够进行删除了。
以下代码:
const obj = { 'name1': 'xxx' }; Object.defineProperty(obj, 'name', { 'value': 'kongzhi', 'writable': true, 'enumerable': true, 'configurable': true }); // 使用delete 删除属性 delete obj.name; console.log(obj.name); // 打印出undefined
如上就是 数据描述符 中的四个配置项的基本含义。那么下面咱们来看看 访问器描述符 的具体用法和含义。
1.2 访问器描述符
访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。以下基本语法:
const obj = {}; Object.defineProperty(obj, 'name', { get: function() {}, set: function(value) {}, configurable: true | false, enumerable: true | false });
注意:使用访问器描述符中 getter或 setter方法的话,不容许使用 writable 和 value 这两个配置项。
getter/setter
当咱们须要设置或获取对象的某个属性的值的时候,咱们可使用 setter/getter方法。
以下代码的使用demo.
const obj = {}; let initValue = 'kongzhi'; Object.defineProperty(obj, 'name', { // 当咱们使用 obj.name 获取该值的时候,会自动调用 get 函数 get: function() { return initValue; }, set: function(value) { initValue = value; } }); // 咱们来获取值,会自动调用 Object.defineProperty 中的 get函数方法。 console.log(obj.name); // 打印出kongzhi // 设置值的话,会自动调用 Object.defineProperty 中的 set方法。 obj.name = 'xxxxx'; console.log(obj.name); // 打印出 xxx
注意:configurable 和 enumerable 配置项和数据描述符中的含义是同样的。
1.3:使用 Object.defineProperty 来实现一个简单双向绑定的demo
以下代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>标题</title> </head> <body> <input type="text" id="demo" /> <div id="xxx">{{name}}</div> <script type="text/javascript"> const obj = {}; Object.defineProperty(obj, 'name', { set: function(value) { document.getElementById('xxx').innerHTML = value; document.getElementById('demo').value = value; } }); document.querySelector('#demo').oninput = function(e) { obj.name = e.target.value; } obj.name = ''; </script> </body> </html>
1.4 Object.defineProperty 对数组的监听
看以下demo代码来理解下对数组的监听的状况。
const obj = {}; let initValue = 1; Object.defineProperty(obj, 'name', { set: function(value) { console.log('set方法被执行了'); initValue = value; }, get: function() { return initValue; } }); console.log(obj.name); // 1 obj.name = []; // 会执行set方法,会打印信息 // 给 obj 中的name属性 设置为 数组 [1, 2, 3], 会执行set方法,会打印信息 obj.name = [1, 2, 3]; // 而后对 obj.name 中的某一项进行改变值,不会执行set方法,不会打印信息 obj.name[0] = 11; // 而后咱们打印下 obj.name 的值 console.log(obj.name); // 而后咱们使用数组中push方法对 obj.name数组添加属性 不会执行set方法,不会打印信息 obj.name.push(4); obj.name.length = 5; // 也不会执行set方法
如上执行结果咱们能够看到,当咱们使用 Object.defineProperty 对数组赋值有一个新对象的时候,会执行set方法,可是当咱们改变数组中的某一项值的时候,或者使用数组中的push等其余的方法,或者改变数组的长度,都不会执行set方法。也就是若是咱们对数组中的内部属性值更改的话,都不会触发set方法。所以若是咱们想实现数据双向绑定的话,咱们就不能简单地使用 obj.name[1] = newValue; 这样的来进行赋值了。那么对于vue这样的框架,那么通常会重写 Array.property.push方法,而且生成一个新的数组赋值给数据,这样数据双向绑定就触发了。
所以咱们须要从新编写数组的push方法来实现数组的双向绑定,咱们能够参照以下方法来理解下。
1) 重写编写数组的方法:
const arrPush = {}; // 以下是 数组的经常使用方法 const arrayMethods = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 对数组的方法进行重写 arrayMethods.forEach((method) => { const original = Array.prototype[method]; arrPush[method] = function() { console.log(this); return original.apply(this, arguments); } }); const testPush = []; // 对 testPush 的原型 指向 arrPush,所以testPush也有重写后的方法 testPush.__proto__ = arrPush; testPush.push(1); // 打印 [], this指向了 testPush testPush.push(2); // 打印 [1], this指向了 testPush
2)使用 Object.defineProperty 对数组方法进行监听操做。
所以咱们须要把上面的代码继续修改下进行使用 Object.defineProperty 进行监听便可:
Vue中的作法以下, 代码以下:
function Observer(data) { this.data = data; this.walk(data); } var p = Observer.prototype; var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ].forEach(function(method) { // 使用 Object.defineProperty 进行监听 Object.defineProperty(arrayMethods, method, { value: function testValue() { console.log('数组被访问到了'); const original = arrayProto[method]; // 使类数组变成一个真正的数组 const args = Array.from(arguments); original.apply(this, args); } }); }); p.walk = function(obj) { let value; for (let key in obj) { // 使用 hasOwnProperty 判断对象自己是否有该属性 if (obj.hasOwnProperty(key)) { value = obj[key]; // 递归调用,循环全部的对象 if (typeof value === 'object') { // 而且该值是一个数组的话 if (Array.isArray(value)) { const augment = value.__proto__ ? protoAugment : copyAugment; augment(value, arrayMethods, key); observeArray(value); } /* 若是是对象的话,递归调用该对象,递归完成后,会有属性名和值,而后对 该属性名和值使用 Object.defindProperty 进行监听便可 */ new Observer(value); } this.convert(key, value); } } } p.convert = function(key, value) { Object.defineProperty(this.data, key, { enumerable: true, configurable: true, get: function() { console.log(key + '被访问到了'); return value; }, set: function(newVal) { console.log(key + '被从新设置值了' + '=' + newVal); // 若是新值和旧值相同的话,直接返回 if (newVal === value) return; value = newVal; } }); } function observeArray(items) { for (let i = 0, l = items.length; i < l; i++) { observer(items[i]); } } function observer(value) { if (typeof value !== 'object') return; let ob = new Observer(value); return ob; } function def (obj, key, val) { Object.defineProperty(obj, key, { value: val, enumerable: true, writable: true, configurable: true }) } // 兼容不支持 __proto__的方法 function protoAugment(target, src) { target.__proto__ = src; } // 不支持 __proto__的直接修改先关的属性方法 function copyAugment(target, src, keys) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i]; def(target, key, src[key]); } } // 下面是测试数据 var data = { testA: { say: function() { console.log('kongzhi'); } }, xxx: [{'a': 'b'}, 11, 22] }; var test = new Observer(data); console.log(test); data.xxx.push(33);