详谈Object.defineProperty 及实现数据双向绑定


Posted in Javascript onJuly 18, 2020

Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截行为,然后进行额外的操作,然后返回结果。那么vue中双向数据绑定就是一个典型的应用。

Vue2.x 是使用 Object.defindProperty(),来进行对对象的监听的。

Vue3.x 版本之后就改用Proxy进行实现的。

下面我们先来理解下Object.defineProperty作用。

一: 理解Object.defineProperty的语法和基本作用。

在理解之前,我们先来看看一个普通的对象,对象它是由多个名/值对组成的无序集合。对象中每个属性对于任意类型的值。

比如现在我们想创建一个简单的对象,可以简单的如下代码:

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 来定义新的属性或修改原有的属性。最终会返回该对象。

接下来我们慢慢来理解下该用法。

基本语法:

Object.defineProperty(obj, prop, descriptor);

基本的参数解析如下:

obj: 可以理解为目标对象。

prop: 目标对象的属性名。

descriptor: 对属性的描述。

那么对于第一个参数obj 和 prop参数,我们很容易理解,比如上面的实列demo,我们定义的 obj对象就是第一个参数的含义,我们在obj中定义的name属性和xxx属性是prop的含义,那么第三个参数描述符是什么含义呢?

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);

以上这篇详谈Object.defineProperty 及实现数据双向绑定就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JAVASCRIPT对象及属性
Feb 13 Javascript
Javascript 通过json自动生成Dom的代码
Apr 01 Javascript
JSON JQUERY模板实现说明
Jul 03 Javascript
基于jquery的鼠标拖动效果代码
May 30 Javascript
jquery实现的随机多彩tag标签随机颜色和字号大小效果
Mar 27 Javascript
js实现浮动在网页右侧的简洁QQ在线客服代码
Sep 04 Javascript
js判断移动端是否安装某款app的多种方法
Dec 18 Javascript
JavaScript实现图片拖曳效果
Sep 08 Javascript
基于js 各种排序方法和sort方法的区别(详解)
Jan 03 Javascript
jQuery三组基本动画与自定义动画操作实例总结
May 09 jQuery
javascript使用正则表达式实现注册登入校验
Sep 23 Javascript
vuex刷新后数据丢失的解决方法
Oct 18 Javascript
webpack+vue-cil 中proxyTable配置接口地址代理操作
Jul 18 #Javascript
Vue项目前后端联调(使用proxyTable实现跨域方式)
Jul 18 #Javascript
完美解决通过IP地址访问VUE项目的问题
Jul 18 #Javascript
Vue移动端项目实现使用手机预览调试操作
Jul 18 #Javascript
vue中移动端调取本地的复制的文本方式
Jul 18 #Javascript
vue中用 async/await 来处理异步操作
Jul 18 #Javascript
vue 使用async写数字动态加载效果案例
Jul 18 #Javascript
You might like
PHP里的中文变量说明
2011/07/23 PHP
PHP实现多进程并行操作的详解(可做守护进程)
2013/06/18 PHP
IIS6.0中配置php服务全过程解析
2013/08/07 PHP
ThinkPHP行为扩展Behavior应用实例详解
2014/07/22 PHP
win7系统配置php+Apache+mysql环境的方法
2015/08/21 PHP
JavaScript与HTML结合的基本使用方法整理
2015/10/12 PHP
自制PHP框架之路由与控制器
2017/05/07 PHP
php 下 html5 XHR2 + FormData + File API 上传文件操作实例分析
2020/02/28 PHP
javascript得到XML某节点的子节点个数的脚本
2008/10/11 Javascript
JavaScript获取GridView中用户点击控件的行号,列号
2009/04/14 Javascript
extjs 04_grid 单击事件新发现
2012/11/27 Javascript
JQuery DataTable删除行后的页面更新利用Ajax解决
2013/05/17 Javascript
node.js中的http.response.addTrailers方法使用说明
2014/12/14 Javascript
JS给Textarea文本框添加行号的方法
2015/08/20 Javascript
深入浅析JavaScript中对事件的三种监听方式
2015/09/29 Javascript
JavaScript实现横线提示输入验证码随输入验证码输入消失的方法
2016/09/24 Javascript
深入了解JavaScript的逻辑运算符(与、或)
2016/12/20 Javascript
JS对象深度克隆实例分析
2017/03/16 Javascript
jquery仿微信聊天界面
2017/05/06 jQuery
微信小程序动态显示项目倒计时效果
2017/06/13 Javascript
详解vue2父组件传递props异步数据到子组件的问题
2017/06/29 Javascript
用JavaScript做简易的购物车的代码示例
2017/10/20 Javascript
Vue代码分割懒加载的实现方法
2017/11/23 Javascript
Vue微信公众号网页分享的示例代码
2020/05/28 Javascript
Python利用itchat库向好友或者公众号发消息的实例
2019/02/21 Python
python实现mask矩阵示例(根据列表所给元素)
2020/07/30 Python
CSS3 简写animation
2012/05/10 HTML / CSS
CSS3+Sprite实现僵尸行走动画特效源码
2016/01/27 HTML / CSS
伦敦最有品味的百货:Liberty London
2016/11/12 全球购物
《乡愁》教学反思
2014/02/18 职场文书
意向书范文
2014/03/31 职场文书
材料专业大学毕业生自荐书
2014/07/02 职场文书
公司员工辞职信范文
2015/05/12 职场文书
2015年领导干部廉洁自律工作总结
2015/05/26 职场文书
《绝招》教学反思
2016/02/20 职场文书
如何利用python创作字符画
2022/06/25 Python